Паралельні (GPU) алгоритми для асинхронних стільникових автоматів


12

У мене є колекція обчислювальних моделей, які можна було б охарактеризувати як асинхронні стільникові автомати. Ці моделі нагадують модель Ізінга, але трохи складніші. Схоже, що такі моделі виграють від запуску на графічному процесорі, а не на процесорі. На жаль, паралелізувати таку модель не зовсім просто, і мені зовсім не зрозуміло, як це робити. Я знаю, що є література з цього приводу, але все це, здається, спрямоване на хардкорних комп'ютерних вчених, які цікавляться деталями алгоритмічної складності, а не на когось, як я, хто просто хоче опис чогось, що я можу реалізувати, і тому я вважаю це досить непроникним.

Для наочності я не шукаю оптимального алгоритму настільки, як щось, що я можу швидко реалізувати в CUDA, що, ймовірно, дасть значне прискорення над моєю реалізацією процесора. Час програміста є набагато більш обмежуючим фактором, ніж час роботи комп'ютера в цьому проекті.

Я також повинен уточнити, що асинхронний стільниковий автомат - це зовсім інша річ, ніж синхронний, і методи паралелізації синхронних КА (наприклад, життя Конвея) не можуть бути легко адаптовані до цієї проблеми. Різниця полягає в тому, що синхронний СА оновлює кожну клітинку одночасно на кожному кроці, тоді як асинхронний оновлює випадкову обрану локальну область на кожному кроці, як зазначено нижче.

Моделі, які я хочу паралелізувати, реалізовані на решітці (зазвичай гексагональній), що складається з ~ 100000 комірок (хоча я хотів би використовувати більше), а непаралелізований алгоритм їх запуску виглядає приблизно так:

  1. Виберіть сусідню пару комірок навмання

  2. Обчисліть "енергетичну" функцію на основі локальної околиці, що оточує ці клітиниΔE

  3. Імовірно, що залежить від (з параметром β ), або поміняйте стани двох комірок, або не зробіть нічого.eβΔEβ

  4. Повторіть описані вище кроки нескінченно.

Існують також деякі ускладнення, пов'язані з граничними умовами, але я думаю, що це не складе великих труднощів для паралелізації.

Варто зазначити, що мене цікавить тимчасова динаміка цих систем, а не просто стан рівноваги, тому мені потрібно щось, що має еквівалентну динаміку до вищевказаного, а не просто щось, що наближатиметься до такого ж рівноважного розподілу. (Отже, варіанти алгоритму шахматної дошки - це не те, що я шукаю.)

Основна складність паралелізації вищевказаного алгоритму - зіткнення. Оскільки всі розрахунки залежать лише від локальної області решітки, для багатьох решітних сайтів можна паралельно оновлюватись до тих пір, поки їхні квартали не перетинаються. Питання в тому, як уникнути таких перекриттів. Я можу придумати декілька способів, але не знаю, який із них найкращий для реалізації. Це такі:

  • Використовуйте центральний процесор для створення списку випадкових сітчастих сайтів та перевірки на колізії. Коли кількість сайтів сітки дорівнює кількості процесорів GPU або якщо виявлено зіткнення, надішліть кожен набір координат до блоку GPU, щоб оновити відповідний сайт сітки. Це було б легко здійснити, але, ймовірно, не дало б великої швидкості, оскільки перевірка на предмет зіткнень на процесорі, ймовірно, не буде набагато дешевшою, ніж все оновлення на процесорі.

  • Розділіть решітку вгору на регіони (по одній на одиницю GPU) і мати один блок GPU, відповідальний за випадковий вибір і оновлення комірок сітки в її регіоні. Але є багато проблем з цією ідеєю, яку я не знаю, як вирішити, найбільш очевидним є те, що саме повинно відбутися, коли підрозділ вибирає околиці, що перекриваються краю свого регіону.

  • Апроксимуйте систему так: нехай час протікає дискретно. Розділіть решітку вгору на іншунабір регіонів на кожному кроці часу відповідно до деякої заздалегідь визначеної схеми, і кожен блок GPU випадковим чином вибирає та оновлює пару комірок сітки, сусідство яких не перекриває межу регіону. Оскільки межі змінюються щоразу на етапі, це обмеження може не надто впливати на динаміку, якщо регіони є відносно великими. Це здається простим у здійсненні та, ймовірно, швидким, але я не знаю, наскільки добре це наблизить динаміку, або яка найкраща схема вибору меж регіону на кожному кроці часу. Я знайшов деякі посилання на "блок-синхронні стільникові автомати", які можуть бути або не збігатися з цією ідеєю. (Я не знаю, бо, здається, що всі описи методу є російською мовою або є джерелами, до яких я не маю доступу.)

Мої конкретні питання такі:

  • Чи є один із перерахованих вище алгоритмів розумним способом підходу до паралелізації GPU асинхронної моделі CA?

  • Чи є кращий спосіб?

  • Чи існує код бібліотеки для такого типу проблем?

  • Де я можу знайти чіткий англомовний опис методу «блок-синхрон»?

Прогрес

Я вважаю, що я придумав спосіб паралелізації асинхронного СА, який може бути відповідним. Алгоритм, описаний нижче, призначений для звичайного асинхронного СА, який оновлює лише одну клітинку за один раз, а не сусідню пару клітин, як це робить моя. Є деякі проблеми з узагальненням цього мого конкретного випадку, але я думаю, що я маю ідею, як їх вирішити. Однак я не впевнений, яку перевагу від швидкості вона дасть із причин, обговорених нижче.

Ідея полягає в заміні асинхронного СА (відтепер ACA) на стохастичний синхронний CA (SCA), який веде себе рівномірно. Для цього спочатку уявляємо, що ACA - це процес Пуассона. Тобто час протікає безперервно, і кожна комірка є постійною ймовірністю за одиницю часу виконання своєї функції оновлення, незалежно від інших комірок.

Xijtijtij(0)Exp(λ)λ - параметр, значення якого можна вибрати довільно.)

На кожному логічному кроці часу осередки SCA оновлюються наступним чином:

  • k,li,jtkl<tij

  • XijXklΔtExp(λ)tijtij+Δt

Я вважаю, що це гарантує, що осередки будуть оновлюватися в порядку, який можна "розшифрувати", щоб відповідати початковому ACA, уникаючи зіткнень та дозволяючи паралельно оновлювати деякі комірки. Однак через першу точку кулі вище, це означає, що більшість процесорів GPU будуть працювати в режимі очікування на кожному кроці SCA, що є менш ніж ідеальним.

Мені потрібно ще трохи подумати про те, чи можна покращити продуктивність цього алгоритму та як розширити цей алгоритм для вирішення випадку, коли декілька комірок оновлюються одночасно в ACA. Однак це виглядає багатообіцяюче, тому я подумав, що опишу його тут на випадок, якщо хто-небудь (а) знає щось подібне в літературі, або (б) може запропонувати будь-яке розуміння цих решти питань.


Можливо, ви можете сформулювати свою проблему за допомогою трафаретного підходу. Багато проблем існує для проблем на основі трафарету. Ви можете подивитися: libgeodecomp.org/gallery.html , «Гра життя» Конвея. Це може мати певну схожість.
vanCompute

@vanCompute, який виглядає як фантастичний інструмент, але, з мого початкового (досить побіжного) дослідження, схоже, парадигма коду трафарету за своєю суттю є синхронною, тому, ймовірно, не дуже підходить до того, що я намагаюся зробити. Однак я детальніше розбираюсь у цьому.
Натаніел

Чи можете ви надати ще кілька деталей про те, як би ви паралелізували це за допомогою SIMT? Ви б використовували одну нитку на пару? Чи може робота, пов’язана з оновленням однієї пари, поширюється на 32 або більше ниток?
Педро

@ Педро роботи, пов’язані з оновленням однієї пари, досить невеликі (в основному просто підведення підсумків по сусідству, плюс одна ітерація генератора випадкових чисел і один exp()), тому я б не подумав, що має сенс розподіляти його по декількох потоках. Я думаю, що краще (і простіше для мене) спробувати і оновити кілька пар паралельно, по одній парі на нитку.
Натаніел

Гаразд, і як визначити збіг між парами оновлень? Якщо пари самі перетинаються, або якщо їх квартали перетинаються?
Педро

Відповіді:


4

Я використовував би перший варіант і використовував би синхронний запуск змінного струму раніше (використовуючи GPU), щоб виявити зіткнення, виконати крок шестикутного змінного струму, правилом якого є значення центральної комірки = Сума (сусіди). Цього ЦО повинно бути сім станів повинні бути ініційовані випадковим чином вибраною коміркою і перевірити їх стан перед запуском правила оновлення для кожної gpu.

Зразок 1. Значення сусідньої комірки поділяється

0 0 0 0 0 0 0

  0 0 1 0 0 0

0 0 0 0 0 0 0

  0 0 0 1 0 0

0 0 0 0 0 0 0

крок ЦА, правилом якого є гексагональна центральна комірка = Сума (сусіди)

0 0 1 1 0 0 0

  0 1 1 1 0 0

0 0 1 2 1 0 0

  0 0 1 1 1 0

0 0 0 1 1 0 0

Зразок 2. Значення комірки для оновлення враховується як сусід з іншого

0 0 0 0 0 0 0

  0 0 1 0 0 0

0 0 0 1 0 0 0

  0 0 0 0 0 0

0 0 0 0 0 0 0

Після ітерації

0 0 1 1 0 0 0

  0 1 2 2 0 0

0 0 2 2 1 0 0

  0 0 1 1 0 0

0 0 0 0 0 0 0

Зразок 3. Немає стосунків

  0 0 0 0 0 0

0 0 1 0 0 0 0

  0 0 0 0 0 0

0 0 0 0 0 0 0

  0 0 0 1 0 0

0 0 0 0 0 0 0

Після ітерації

  0 1 1 0 0 0

0 1 1 1 0 0 0

  0 1 1 0 0 0

0 0 0 1 1 0 0

  0 0 1 1 1 0

0 0 0 1 1 0 0


O(n)n

Я думаю, що можна багато чого зробити паралельним. Обробка зіткнення здійснюється повністю на GPU - це етап синхронного змінного струму, як показано у посиланні, розміщеному вище. для перевірки буде використано локальне правило, якщо сума (сусіди) = 8 НЕ зіткнення, сума (сусіди)> 8 Зіткнення, це буде перевірено перед запуском зміни вашого правила оновлення, якщо немає стану комірок зіткнення, оскільки обидва слід розмістити біля бали, які повинні бути оцінені, якщо вони не є близькими, належать до інших комірок.
jlopez1967

Я це розумію, але проблема полягає в тому, що ви робите, коли виявляєте зіткнення? Як я пояснював вище, ваш алгоритм CA - це лише перший крок у виявленні зіткнення. Другий крок - пошук сітки для комірок зі станом> = 2, і це не тривіально.
Натаніел

наприклад, Уявіть, що ми хочемо виявити комірку зіткнення (5.7), на стільникових автоматах і виконану суму (сусіди комірки (5,7)), і якщо значення 8, а якщо немає зіткнення, то більше 8 немає зіткнення це повинна бути у функції, що оцінює кожну клітинку, щоб визначити наступний стан комірки в асинхронних стільникових автоматах. Виявлення зіткнення для кожної комірки - місцеве правило, яке стосується лише сусідніх комірок
jlopez1967,

Так, але на питання, на яке нам потрібно мати можливість відповісти, щоб паралелізувати асинхронний СА, це не «чи було зіткнення в комірці (5,7)», а «чи було зіткнення десь на сітці, і якщо так, то де це? " На це не можна відповісти без повторення через сітку.
Натаніел

1

Виходячи з ваших відповідей на мої запитання в коментарях вище, я б запропонував спробувати підхід на основі блокування, в якому кожен потік намагається зафіксувати околиці, які буде оновлено, перш ніж обчислити фактичне оновлення.

Це можна зробити за допомогою атомних операцій, передбачених у CUDA, та масиву, intщо містить замки для кожної комірки, наприклад lock. Кожна нитка виконує наступні дії:

ci, cj = choose a pair at random.

int locked = 0;

/* Try to lock the cell ci. */
if ( atomicCAS( &lock[ci] , 0 , 1 ) == 0 ) {

    /* Try to lock the cell cj. */
    if ( atomicCAS( &lock[cj] , 0 , 1 ) == 0 ) {

        /* Now try to lock all the neigbourhood cells. */
        for ( cn = indices of all neighbours )
            if ( atomicCAS( &lock[cn] , 0 , 1 ) != 0 )
                break;

        /* If we hit a break above, we have to unroll all the locks. */
        if ( cn < number of neighbours ) {
            lock[ci] = 0;
            lock[cj] = 0;
            for ( int i = 0 ; i < cn ; i++ )
                lock[i] = 0;
            }

        /* Otherwise, we've successfully locked-down the neighbourhood. */
        else
            locked = 1;

        }

    /* Otherwise, back off. */
    else
        lock[ci] = 0;
    }

/* If we got everything locked-down... */
if ( locked ) {

    do whatever needs to be done...

    /* Release all the locks. */
    lock[ci] = 0;
    lock[cj] = 0;
    for ( int i = 0 ; i < cn ; i++ )
        lock[i] = 0;

    }

Зауважимо, що цей підхід, мабуть, не найоптимальніший, але може дати цікаву відправну точку. Якщо між нитками виникає багато зіткнень, тобто одна чи більше на 32 нитки (як при одному зіткненні за основу), тоді буде справедливий біт відхилення гілок. Крім того, атомні операції можуть бути трохи повільними, але оскільки ви робите лише операції порівняння та заміни, це має масштабувати гаразд.

Замикаючі накладні можуть виглядати страхітливими, але насправді це лише кілька завдань і гілок, не набагато більше.

Зауважте також, що я швидко і розгублено нотуюся в петлі iнад сусідами.

Додаток: Я був досить кавалером, щоб припустити, що ви могли просто відступити, коли пари стикаються. Якщо це не так, то ви можете обгорнути все, як у другому рядку, у while-loop та додати a breakв кінці підсумкової ifзаяви.

Після цього всі потоки доведеться почекати, поки не буде зроблено останнє, але якщо зіткнення трапляються рідко, ви повинні мати можливість піти з ним.

Додаток 2: Do НЕ спокуситися додати виклики __syncthreads()в коді, особливо це зацикленої версія описана в попередньому додатку! Асинхронність є важливою для уникнення повторних зіткнень в останньому випадку.


Дякую, це виглядає досить добре. Напевно, краще, ніж складна ідея, яку я розглядав, і набагато простіше втілити в життя. Я можу зробити зіткнення рідкісними, використовуючи досить велику сітку, що, мабуть, добре. Якщо метод простого відключення виявиться значно швидшим, я можу використовувати його для неофіційного дослідження параметрів та переключитися на метод очікування для всіх інших людей, коли мені потрібно отримати офіційні результати. Я спробую це спробувати найближчим часом.
Натаніел

1

Я провідний розробник LibGeoDecomp. Хоча я погоджуюся з vanCompute, що ви могли б емулювати ваш ACA з CA, ви праві, що це було б не дуже ефективно, оскільки лише деякі клітини на будь-якому етапі мають бути оновлені. Це дійсно дуже цікавий додаток - і цікаво повозитися!

Я б запропонував вам поєднати рішення, запропоновані jlopez1967 та Pedro: Алгоритм Педро добре фіксує паралелізм, але ці атомні замки жахливо повільні. Рішення jlopez1967 є елегантним, коли йдеться про виявлення зіткнень, але перевірка всіх nкомірок, коли kактивні лише менші підмножини (я буду вважати, що є деякий параметр, який позначає кількість осередків, що оновлюються одночасно), явно заборонний.

__global__ void markPoints(Cell *grid, int gridWidth, int *posX, int *posY)
{
    int id = blockIdx.x * blockDim.x + threadIdx.x;
    int x, y;
    generateRandomCoord(&x, &y);
    posX[id] = x;
    posY[id] = y;
    grid[y * gridWidth + x].flag = 1;
}

__global__ void checkPoints(Cell *grid, int gridWidth, int *posX, int *posY, bool *active)
{
    int id = blockIdx.x * blockDim.x + threadIdx.x;
    int x = posX[id];
    int y = posY[id];
    int markedNeighbors = 
        grid[(y - 1) * gridWidth + x + 0].flag +
        grid[(y - 1) * gridWidth + x + 1].flag +
        grid[(y + 0) * gridWidth + x - 1].flag +
        grid[(y + 0) * gridWidth + x + 1].flag +
        grid[(y + 1) * gridWidth + x + 0].flag +
        grid[(y + 1) * gridWidth + x + 1].flag;
    active[id] = (markedNeighbors > 0);
}


__global__ void update(Cell *grid, int gridWidth, int *posX, int *posY, bool *active)
{
    int id = blockIdx.x * blockDim.x + threadIdx.x;
    int x = posX[id];
    int y = posY[id];
    grid[y * gridWidth + x].flag = 0;
    if (active[id]) {
        // do your fancy stuff here
    }
}

int main() 
{
  // alloc grid here, update up to k cells simultaneously
  int n = 1024 * 1024;
  int k = 1234;
  for (;;) {
      markPoints<<<gridDim,blockDim>>>(grid, gridWidth, posX, posY);
      checkPoints<<<gridDim,blockDim>>>(grid, gridWidth, posX, posY, active);
      update<<<gridDim,blockDim>>>(grid, gridWidth, posX, posY, active);
  }
}

За відсутності хорошої глобальної синхронізації на графічному процесорі, потрібно викликати кілька ядер для різних фаз. У Kepler від Nvidia ви можете перемістити навіть основний цикл до графічного процесора, але я не сподіваюся, що це набере багато.

Алгоритми досягають (настроюється) ступеня паралельності. Гадаю, цікаве питання - чи зіткнення вплинуть на ваш випадковий розподіл при збільшенні k.


0

Я пропоную вам ознайомитись із цим посиланням http://www.wolfram.com/training/courses/hpc021.html приблизно за 14:15 хвилин до відео, звичайно, навчання з математики, де вони здійснюють реалізацію стільникових автоматів за допомогою CUDA , звідти ви можете це змінити.


На жаль, це синхронний СА, який є досить різним типом звіра від асинхронних, з якими я маю справу. У синхронному CA кожен осередок оновлюється одночасно, і це легко паралелізувати GPU, але в асинхронному CA одна випадково обрана комірка оновлюється щоразу, коли це відбувається (насправді в моєму випадку це дві сусідні комірки), і це робить паралелізація набагато важче. Проблеми, викладені в моєму питанні, стосуються необхідності асинхронної функції оновлення.
Натаніел
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.