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

Кои са най-добрите практики, които трябва да имате предвид, когато внедрявате функция за грешка, дефинирана като

формула за грешка

използвайки OpenCL ядро?

A, B и C са триизмерни плаващи масиви, а \delta е делтата на Kronecker.

Типични стойности за (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

След векторизиране на кода, преместване на битове в локалната памет, разгръщане на някои цикли и предаване на предварително изчислени продукти на Kronecker изрично към ядрото, кодът изглежда по следния начин:

__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;
}

Чрез прилагането на тези промени се наблюдава 4x увеличение на скоростта в сравнение с простото изпълнение. Освен това беше разкрито, че най-скъпата линия от всички е актуализацията на грешката, т.е.

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% от общия аритметичен пиков капацитет на вашия GPU, така че не би трябвало да е голяма изненада, че е бавен.....   -  person talonmies    schedule 02.06.2012
comment
@talonmies: не, всеки работен елемент работи върху отделен екземпляр на проблема.   -  person user92382    schedule 02.06.2012
comment
@user92382 колко работни елемента има? (т.е. какъв е обхватът за индекса?) Ако това не е в стотици или хиляди, тогава помислете за разпръскване на някои от циклите, особено тези с кратка матрица, в 2D или 3D работни елементи.   -  person pmdj    schedule 02.06.2012
comment
@pmjordan: индексът е в диапазона от 20 000 до 500 000   -  person user92382    schedule 02.06.2012
comment
@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)


Обикновено бихте искали да прекъснете някои от тези цикли... много... - външните цикли се разделят на множество workgroups, които работят на своя собствена изчислителна единица (има около 16 изчислителни единици на GPU, не много) - следващите няколко цикъла ще бъдат разделени на различни нишки във всяка работна група

Ако се опитате да изпълните всички изчисления едновременно, всички те ще се опитат да заредят данните в паметта едновременно и това просто ще се разбие ужасно. Графичните процесори имат много ограничена памет. Разбира се, глобалната памет звучи достатъчно голяма, няколко гигабайта, но глобалната GPU памет е бавна. Искате да получите данните в локалната памет, която е на изчислителна единица и е от порядъка на 32-64KB, не много повече от това.

Обикновено бихте искали по някакъв начин да разделите задачата си на много малки задачи и да направите следното за всяка работна група:

  • 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