Рекомендации по распределению / инициализации портативной многоядерной / 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, но это меняет процесс выделения (который, я считаю, необходим).
  • РЕДАКТИРОВАТЬ: мое предыдущее утверждение об операторе newбыло неверным, он может поддерживать несколько аргументов, см. Ответ Четана. Я полагаю, что все еще существует проблема получения библиотек или контейнеров STL для использования указанного соответствия. Несколько полей могут быть упакованы, и может быть неудобно гарантировать, например, std::vectorперераспределение с правильным активным менеджером контекста.
  • Каждый поток может выделить и сбросить свою собственную личную память, но тогда индексация в соседние регионы будет более сложной. (Рассмотрим разреженное матрично-векторное произведение с разделением строки матрицы и векторов; для индексации неизвестной части x требуется более сложная структура данных, когда x не является смежным в виртуальной памяти.)YAИксИксИкс

Какие-либо решения для распределения / инициализации NUMA считаются идиоматическими? Я пропустил другие критические ошибки?

(Я не имею в виду для моего C примеры ++ подразумевает акцент на том языке, однако C ++ язык кодирует некоторые решения об управлении памятью , что язык , как C не происходит , таким образом , существует тенденция к более сопротивления , когда предполагая , что C ++ программисты делают те , все по-другому.)

Джед браун
источник

Ответы:

7

Одним из решений этой проблемы, которое я предпочитаю, является разукрупнение потоков и задач (MPI) на уровне контроллера памяти. То есть удалите аспекты NUMA из своего кода, добавив одну задачу на сокет ЦП или контроллер памяти, а затем добавьте потоки под каждую задачу. Если вы сделаете это таким образом, тогда вы сможете безопасно привязать всю память к этому сокету / контроллеру либо через первое касание, либо через один из доступных API, независимо от того, какой поток на самом деле выполняет распределение или инициализацию. Передача сообщений между сокетами обычно довольно хорошо оптимизирована, по крайней мере, в MPI. У вас всегда может быть больше задач MPI, чем это, но из-за проблем, которые вы поднимаете, я редко рекомендую людям иметь меньше.

Билл Барт
источник
1
Это практическое решение, но даже несмотря на то, что мы быстро получаем больше ядер, число ядер на узел NUMA остается примерно равным 4. На гипотетическом 1000-ядерном узле мы будем запускать 250 процессов MPI? (Это было бы здорово, но я настроен скептически.)
Джед Браун
Я не согласен с тем, что количество ядер в NUMA остается на прежнем уровне. У Sandy Bridge E5 есть 8. У Magny Cours 12. У меня есть узел Westmere-EX с 10. У Interlagos (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 ( Кеплер) СМ смотрят на плоскую память).
Джед Браун
Хороший вызов на чипах AMD. Я забыл. Тем не менее, я думаю, вы еще долго увидите рост в этой области.
Билл Барт
6

Этот ответ является ответом на два неправильных представления C ++ в этом вопросе.

  1. «То же самое относится к новому оператору C ++, который настаивает на инициализации новых распределений (включая POD)»
  2. «Оператор C ++ new принимает только один параметр»

Это не прямой ответ на многоядерные проблемы, которые вы упоминаете. Просто отвечаю на комментарии, которые классифицируют программистов на C ++ как фанатиков C ++, чтобы поддерживать репутацию;).

К пункту 1. C ++ «новое» или выделение стека не требует инициализации новых объектов, независимо от того, POD или нет. Конструктор класса по умолчанию, как определено пользователем, несет эту ответственность. Первый код ниже показывает ненужную распечатку, независимо от того, является ли класс POD или нет.

К пункту 2. C ++ позволяет перегружать «new» несколькими аргументами. Второй код ниже показывает такой случай для выделения отдельных объектов. Это должно дать представление и, возможно, быть полезным для вашей ситуации. Оператор 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';
}

Компилятор Intel 11.1 показывает этот вывод (который, конечно, является неинициализированной памятью, отмеченной «а»).

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, для не-POD массивов , таких как , кроме std::complexкоторых являются явно инициализированы.
Джед Браун
1
@JedBrown: причина № 6, чтобы избежать использования std::complex?
Джек Полсон
1

В сделке. II у нас есть программная инфраструктура для распараллеливания сборки в каждой ячейке на несколько ядер с использованием потоковых строительных блоков (по сути, у вас есть одна задача на ячейку, и вам нужно запланировать эти задачи на доступные процессоры - это не так реализовано но это общая идея). Проблема заключается в том, что для локальной интеграции вам нужно несколько временных (чистых) объектов, и вам нужно предоставить как минимум столько же задач, сколько может выполняться параллельно. Мы видим низкое ускорение, возможно, потому, что когда задача помещается в процессор, она захватывает один из чистых объектов, который обычно находится в кэше другого ядра. У нас было два вопроса:

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

(ii) Как я могу узнать, где я нахожусь, где находится каждый из «чистых» объектов, и какой «чистый» объект мне нужно взять, чтобы получить доступ к тому, который горячий в кеше моего текущего ядра?

В конечном счете, мы не нашли ответов ни на одно из этих решений, и после нескольких работ решили, что нам не хватает инструментов для исследования и решения этих проблем. Я знаю, как, по крайней мере, в принципе решить проблему (ii) (а именно, использовать локальные объекты потока, предполагая, что потоки остаются закрепленными на ядрах процессора - еще одна гипотеза, которая не является тривиальной для тестирования), но у меня нет инструментов для тестирования проблемы (я).

Таким образом, с нашей точки зрения, работа с NUMA остается нерешенным вопросом.

Вольфганг Бангерт
источник
Вы должны привязать свои потоки к сокетам, чтобы вам не приходилось задумываться о том, закреплены ли процессоры. Linux любит перемещать вещи.
Билл Барт
Кроме того, выборка getcpu () или sched_getcpu () (в зависимости от вашего libc и ядра и всего такого) должна позволить вам определить, где работают потоки в Linux.
Билл Барт
Да, и я думаю, что строительные блоки потоков, которые мы используем для планирования работы потоков, связывают потоки с процессорами. Вот почему мы попытались работать с локальным хранилищем потоков. Но мне все еще трудно найти решение моей проблемы (i).
Вольфганг Бангерт
1

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

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

Вы можете найти краткую презентацию с изложением ISC'13 « LIKWID - Инструменты облегченной производительности », и авторы опубликовали статью на Arxiv « Лучшие практики для HPM-инжиниринга производительности на современных многоядерных процессорах ». В этой статье описан подход к интерпретации данных со счетчиков оборудования для разработки производительного кода, специфичного для архитектуры вашего компьютера и топологии памяти.

eoinbrazil
источник
LIKWID полезен, но вопрос был больше о том, как писать числовые / чувствительные к памяти библиотеки, которые могут надежно получать и самостоятельно проверять ожидаемую локальность в разнообразных средах выполнения, схемах потоков, управлении ресурсами MPI и настройке соответствия, использовать с другие библиотеки и т. д.
Джед Браун