Передовий досвід розподілу / ініціалізації переносу багатоядерної / NUMA пам'яті


17

Коли обчислення з обмеженою пропускною здатністю пам’яті виконуються в середовищах спільної пам’яті (наприклад, з потоком через OpenMP, Pthreads або TBB), існує дилема, як забезпечити правильну розподіл пам’яті по фізичній пам’яті, таким чином, щоб кожен потік в основному отримував доступ до пам’яті "локальна" шина пам'яті. Хоча інтерфейси не є портативними, у більшості операційних систем є способи встановлення спорідненості потоків (наприклад, pthread_setaffinity_np()для багатьох систем POSIX, sched_setaffinity()для Linux, SetThreadAffinityMask()для Windows). Існують також такі бібліотеки, як hwloc для визначення ієрархії пам'яті, але, на жаль, більшість операційних систем ще не надають способи встановлення політики пам'яті NUMA. Linux є помітним винятком з libnumaдозволяючи додатку маніпулювати політикою пам'яті та міграцією сторінок при деталізації сторінки (в основному з 2004 року, таким чином, широко доступний). Інші операційні системи очікують, що користувачі дотримуються неявної політики "першого дотику".

Робота з політикою "першого дотику" означає, що абонент повинен створювати та поширювати потоки з будь-якою спорідненістю, яку вони планують використовувати пізніше під час першого запису до щойно виділеної пам'яті. (Дуже небагато систем налаштовано таким чином, що malloc()насправді знаходить сторінки. Він просто обіцяє їх знайти, коли вони фактично винні, можливо, різними потоками.) Це означає, що виділення з використанням calloc()або негайної ініціалізації пам'яті після розподілу з використанням memset()шкідливо, оскільки воно буде схильне до помилок вся пам'ять на шині пам'яті в ядрі, що працює з розподільним потоком, що призводить до найгіршого пропускної здатності пам'яті, коли доступ до пам'яті здійснюється з декількох потоків. Те саме стосується newоператора C ++, який наполягає на ініціалізації багатьох нових розподілів (наприклад,std::complex). Деякі спостереження щодо цього середовища:

  • Виділення можна зробити «колективними нитками», але тепер розподіл стає змішаним у моделі нарізки, що небажано для бібліотек, яким, можливо, доведеться взаємодіяти з клієнтами, використовуючи різні моделі різьблення (можливо, кожна з власними пулами потоків).
  • RAII вважається важливою частиною ідіоматичного C ++, але він, здається, є активно шкідливим для роботи пам'яті в середовищі NUMA. Розміщення newможе використовуватися з пам'яттю, виділеною через malloc()або підпрограми libnuma, але це змінює процес розподілу (що, на мою думку, є необхідним).
  • EDIT: Моя раніше заява про оператора newбула неправильною, вона може підтримувати кілька аргументів, див. Відповідь Четана. Я вважаю, що все ще існує занепокоєння щодо використання бібліотеками або контейнерами STL для використання зазначеної спорідненості. Може бути запаковано кілька полів, і це може бути незручно для того, щоб, наприклад, std::vectorперерозподіляти з активним менеджером контексту активний перенос.
  • Кожен потік може виділяти та помиляти власну приватну пам'ять, але тоді індексація в сусідні регіони є складнішою. (Розглянемо розріджений векторний матричний добуток із розділом рядків матриці та векторами; для індексації невідомій частині x потрібна складніша структура даних, коли x не є суміжним у віртуальній пам'яті.)уАххх

Чи вважаються якісь рішення розподілу / ініціалізації NUMA ідіоматичними? Чи залишив я інші критичні проблеми?

(Я не маю в виду для мого C приклади ++ має на увазі акцент на тій мові, однак C ++ мову кодує деякі рішення про управління пам'яттю , що мова , як C НЕ відбувається , таким чином , існує тенденція до більш опору , коли припускаючи , що C ++ програмісти роблять ті , речі інакше.)

Відповіді:


7

Одним із варіантів вирішення цієї проблеми, якому я, як правило, віддаю перевагу, є розмежування потоків та (MPI) завдань на ефективному рівні контролера пам'яті. Тобто, видаліть аспекти NUMA зі свого коду, маючи одну задачу на сокет процесора або контролер пам'яті, а потім потоки під кожною задачею. Якщо ви зробите це таким чином, ви повинні мати змогу безпечно прив’язати всю пам'ять до цього сокета / контролеру або за допомогою першого дотику, або одного з доступних API, незалежно від того, який потік справді виконує роботу розподілу чи ініціалізації. Повідомлення, що проходять між сокетами, зазвичай досить добре оптимізовані, як мінімум, у MPI. Ви завжди можете мати більше завдань MPI, ніж це, але через проблеми, які ви ставите, я рідко рекомендую людям менше.


1
Це практичне рішення, але, хоча ми швидко отримуємо більше ядер, кількість ядер на вузлі NUMA досить застоюється приблизно на 4. Отже, на гіпотетичному 1000 ядерному вузлі ми будемо запускати 250 MPI-процесів? (Це було б чудово, але я скептично.)
Джед Браун

Я не згоден з тим, що кількість ядер на NUMA застоюється. Піщаний міст E5 має 8. Magny Cours мав 12. У мене вузол Westmere-EX з 10. Інтерлагос (ORNL Titan) має 20. Knights Corner матиме більше 50. Я б здогадався, що ядра на NUMA зберігаються в ногу з Законом Мура, більш-менш.
Білл Барт

Magny Cours та Interlagos мають два відмирання в різних регіонах NUMA, таким чином, 6 та 8 ядер на область NUMA. Перемотування назад до 2006 року, коли два розетки чотирьохядерного Clovertown поділяли б один і той же інтерфейс (чіпсет Blackford) на пам'ять, і мені це не здається, як кількість ядер в регіоні NUMA так швидко зростає. Blue Gene / Q розширює цю плоску пам’ять пам’яті трохи далі, і, можливо, Knight's Corner зробить ще один крок (хоча це вже інший пристрій, тож, можливо, нам слід порівняти з графічними процесорами замість них 15 (Fermi) або 8 ( Kepler) SMs для перегляду плоскої пам'яті).
Джед Браун

Хороший дзвінок на чіпах AMD. Я забув. Тим не менш, я думаю, що ви ще деякий час будете спостерігати зростання в цій галузі.
Білл Барт

6

Ця відповідь відповідає на два помилки, пов'язані з C ++ у запитанні.

  1. "Те саме стосується нового оператора C ++, який наполягає на ініціалізації нових розподілів (включаючи ПОД)"
  2. "Оператор C ++ new приймає лише один параметр"

Це не пряма відповідь на багатозначні питання, які ви згадуєте. Просто відповідаючи на коментарі, які класифікують програмістів на C ++ як ревних С ++, щоб репутація зберігалася;).

До пункту 1. C ++ "новий" або розподіл стека не наполягає на ініціалізації нових об'єктів, будь то PODS чи ні. Конструктор за замовчуванням класу, визначений користувачем, несе таку відповідальність. У першому коді нижче показано надрукований мотлох, чи є клас POD чи ні.

До пункту 2. C ++ дозволяє перевантажувати "нове" декількома аргументами. Другий код нижче показує такий випадок для виділення окремих об'єктів. Це повинно дати ідею і, можливо, бути корисним для ситуації, що склалася. Оператор new [] також може бути змінений відповідним чином.

// Код для пункту 1.

#include <iostream>

struct A
{
    // int/double/char/etc not inited with 0
    // with or without this constructor
    // If present, the class is not POD, else it is.
    A() { }

    int i;
    double d;
    char c[20];
};

int main()
{
    A* a = new A;
    std::cout << a->i << ' ' << a->d << '\n';
    for(int i = 0; i < 20; ++i)
        std::cout << (int) a->c[i] << '\n';
}

Компілятор 11.1 від Intel показує цей вихід (що, звичайно, є неініціалізованою пам'яттю, вказаною "a").

993001483 6.50751e+029
105
108
... // skipped
97
108

// Код для пункту 2.

#include <cstddef>
#include <iostream>
#include <new>

// Just to use two different classes.
class arena { };
class policy { };

struct A
{
    void* operator new(std::size_t, arena& arena_obj, policy& policy_obj)
    {
        std::cout << "special operator new\n";
        return (void*)0x1234; //Just to test
    }
};

void* operator new(std::size_t, arena& arena_obj, policy& policy_obj)
{
    std::cout << "special operator new (global)\n";
    return (void*)0x5678; //Just to test
}

int main ()
{
    arena arena_obj;
    policy policy_obj;
    A* ptr = new(arena_obj, policy_obj) A;
    int* iptr = new(arena_obj, policy_obj) int;
    std::cout << ptr << "\n";
    std::cout << iptr << "\n";
}

Дякуємо за виправлення. Здається, що C ++ не представляє додаткових ускладнень відносно C, за винятком таких масивів, std::complexякі не є POD, такі, які явно ініціалізуються.
Джед Браун

1
@JedBrown: Причина № 6, щоб уникнути використання std::complex?
Джек Поульсон

1

У deal.II у нас є програмна інфраструктура для паралельного складання кожної комірки на декілька ядер за допомогою Threading Building Blocks (по суті, у вас є одне завдання на комірку і потрібно планувати ці завдання на доступних процесорах - це не так, як це реалізовано, але це загальна ідея). Проблема полягає в тому, що для локальної інтеграції вам потрібна кількість тимчасових (подряпин) об’єктів і вам потрібно надати принаймні стільки, скільки є завдань, які можуть виконуватися паралельно. Ми бачимо погану швидкість, імовірно, тому, що коли завдання ставиться на процесор, він захоплює один з об'єктів подряпин, який, як правило, знаходиться в кеші інших ядер. У нас було два питання:

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

(ii) Як я можу дізнатись, де я знаходжуся, де кожен із об’єктів подряпин та який об’єкт скретч я повинен взяти, щоб отримати доступ до того, який є гарячим у кеші мого поточного ядра?

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

Отже, з нашого погляду, робота з NUMA все ще залишається невирішеним питанням.


Ви повинні прив’язати свої потоки до розеток, щоб не потрібно було замислюватися, чи процесори закріплені. Linux любить переміщувати речі.
Білл Барт

Крім того, вибірки getcpu () або sched_getcpu () (залежно від вашого libc та ядра та чого іншого) повинні дозволяти визначати, де потоки працюють у Linux.
Білл Барт

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

1

Поза hwloc є кілька інструментів, які можуть звітувати про середовище пам’яті кластера HPC і які можна використовувати для встановлення різноманітних конфігурацій NUMA.

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

Ви можете ознайомитись з короткою презентацією з ISC'13 " LIKWID - Інструменти легкої продуктивності ", і автори опублікували документ про Arxiv " Кращі практики інженерії продуктивності HPM на сучасному багатоядерному процесорі ". У цьому документі описаний підхід до інтерпретації даних з апаратних лічильників для розробки виконавського коду, характерного для архітектури та топології пам'яті.


LIKWID корисний, але питання стосувалося більше того, як писати бібліотеки, що залежать від пам’яті чи пам’яті, які можуть надійно отримувати та самостійно перевіряти очікувану локальність у різноманітних середовищах виконання, схемах нарізки, керуванні ресурсами MPI та налаштування афінності, використовувати з інші бібліотеки тощо
Джед Браун
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.