Я экспериментирую с NUMA на машине с 4 процессорами Operton 6272, работающими под управлением centOS. Есть 8 узлов NUMA, каждый с 16 ГБ памяти.
Вот небольшая тестовая программа, которую я запускаю.
void pin_to_core(size_t core)
{
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(core, &cpuset);
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
}
int main()
{
pin_to_core( 0 );
size_t bufSize = 100;
for( int i = 0; i < 131000; ++i )
{
if( !(i % 10) )
{
std::cout << i << std::endl;
long long free = 0;
for( unsigned j = 0; j < 8; ++j )
{
numa_node_size64( j, &free );
std::cout << "Free on node " << j << ": " << free << std::endl;
}
}
char* buf = (char*)numa_alloc_onnode( bufSize, 5 );
for( unsigned j = 0; j < bufSize; ++j )
buf[j] = j;
}
return 0;
}
Таким образом, в основном поток, работающий на ядре # 0, выделяет 10000-байтовые буферы 131 на NUMA-узле 5, инициализирует их с мусором и пропускает их. Каждые 10 итераций мы выводим информацию о том, сколько памяти доступно на каждом узле NUMA.
В начале вывода получаю:
0
Free on node 0: 16115879936
Free on node 1: 16667398144
Free on node 2: 16730402816
Free on node 3: 16529108992
Free on node 4: 16624508928
Free on node 5: 16361529344
Free on node 6: 16747118592
Free on node 7: 16631336960
...
И в конце я получаю:
Free on node 0: 15826657280
Free on node 1: 16667123712
Free on node 2: 16731033600
Free on node 3: 16529358848
Free on node 4: 16624885760
Free on node 5: 16093630464
Free on node 6: 16747384832
Free on node 7: 16631332864
130970
Free on node 0: 15826657280
Free on node 1: 16667123712
Free on node 2: 16731033600
Free on node 3: 16529358848
Free on node 4: 16624885760
Free on node 5: 16093630464
Free on node 6: 16747384832
Free on node 7: 16631332864
mbind: Cannot allocate memory
mbind: Cannot allocate memory
mbind: Cannot allocate memory
mbind: Cannot allocate memory
mbind: Cannot allocate memory
mbind: Cannot allocate memory
mbind: Cannot allocate memory
130980
...
Вещи, которые мне не понятны
1) Почему появляются эти сообщения «mbind: Cannot распределять память»? Тот факт, что я далек от использования всей памяти, и поведение не изменится, если я изменю размер буфера, скажем, на 1000, что заставляет меня думать, что у меня заканчиваются какие-то дескрипторы ресурсов ядра ,
2) Несмотря на то, что я попросил выделить память на узле 5, кажется, что фактическое распределение было разделено между узлами 0 и 5.
Может ли кто-нибудь, пожалуйста, дать понять, почему это происходит?
ОБНОВИТЬ
Хотел бы дать более подробную информацию по пункту (2). Тот факт, что часть памяти не выделена на узле 5, похоже, имеет какое-то отношение к тому факту, что мы инициализируем буфер на ядре # 0 (которое принадлежит узлу NUMA 0). Если я изменю pin_to_core(0)
в pin_to_core(8)
затем выделенная память распределяется между узлами 1 и 5. Если это pin_to_core(40)
тогда вся память выделяется на узле 5.
UPDATE2
Я посмотрел на исходный код libnuma и попытался заменить вызов numa_alloc_onnode()
с более низкоуровневыми звонками оттуда: mmap()
а также mbind()
, Теперь я также проверяю, на каком узле NUMA находится память — я использую move_pages()
призыв к этому. Результаты приведены ниже. Перед инициализацией (цикл завершен j
) страница не отображается ни на один узел (я получаю код ошибки ENOENT), и после инициализации страница назначается либо узлу 0, либо узлу 5. Шаблон является регулярным: 5,0,5,0, … Как и раньше когда мы приближаемся к 131000-й итерации, вызовы mbind()
начинайте возвращать коды ошибок, и когда это происходит, страница всегда выделяется узлу 0. Код ошибки, возвращаемый mbind, — это ENOMEM, в документации сказано, что это означает, что «память ядра» исчерпана. Я не знаю, что это такое, но это не может быть «физическая» память, потому что у меня 16ГБ на узел.
Итак, вот мои выводы:
Ограничения на отображение памяти, наложенные mbind()
удерживаются только в 50% случаев, когда ядро другого узла NUMA первым касается памяти. Я хотел бы, чтобы это было задокументировано где-то, потому что тихо нарушать обещание не приятно …
Существует ограничение на количество звонков mbind
, Так что надо mbind()
большие куски памяти, когда это возможно.
Подход, который я собираюсь попробовать, заключается в следующем: выполнять задачи выделения памяти в потоках, которые прикреплены к ядрам определенных NUMA-приложений. Для дополнительного спокойствия я попытаюсь позвонить mlock (из-за описанных проблем Вот).
Как вы уже узнали из чтения libnuma.c
каждый звонок numa_alloc_onnode()
создает новую анонимную карту памяти и затем связывает область памяти с указанным узлом NUMA. С таким количеством призывов mmap()
вы просто достигаете максимально допустимого числа отображений памяти на процесс. Значение может быть прочитано из /proc/sys/vm/max_map_count
а также может быть изменен системным администратором путем записи в псевдофайл:
# echo 1048576 > /proc/sys/vm/max_map_count
или с sysctl
:
# sysctl -w vm.max_map_count=1048576
По умолчанию на майских дистрибутивах Linux 65530
отображения. mmap()
реализует объединение отображений, то есть сначала пытается расширить существующее отображение перед созданием нового. В моих тестах он создает новое отображение при каждом втором вызове, а в остальном расширяет предыдущий. Перед первым звонком numa_alloc_onnode()
мои тестовые процессы имеют 37 отображений. Следовательно mmap()
должен начать терпеть неудачу где-то после 2 * (65530-37) = 130986
звонки.
Это выглядит так, когда mbind()
применяется к части существующего отображения, происходит что-то странное, и вновь затронутая область не связана должным образом. Я должен копаться в исходном коде ядра, чтобы выяснить, почему. С другой стороны, если вы замените:
numa_alloc_onnode( bufSize, 5 )
с
numa_alloc_onnode( bufSize, i % 4 )
объединение отображений не выполняется и mmap()
терпит неудачу около 65500-й итерации, и все распределения правильно связаны.
На ваш первый вопрос со страницы руководства numa_alloc_onnode
The size argument will be rounded up to a multiple of the system page size.
Это означает, что, хотя вы запрашиваете небольшой объем данных, вы получаете целые страницы. Тем не менее, в вашей программе вы на самом деле запрашиваете 131000 системных страниц.
Для вашего второго вопроса я предлагаю использовать numa_set_strict()
заставить numa_alloc_onnode
потерпеть неудачу, если не может выделить страницу на данном узле.
numa_set_strict() sets a flag that says whether the functions allocating
on specific nodes should use use a strict policy. Strict means the
allocation will fail if the memory cannot be allocated on the target
node. Default operation is to fall back to other nodes. This doesn't
apply to interleave and default.