Я новичок в теме NUMA. Я также должен сказать, что я программист и не обладаю глубокими знаниями аппаратного обеспечения.
Я работаю на сервере Quad Operton 6272. Материнская плата SuperMicro H8QGi + -F, в общей сложности 132 ГБ памяти (8 16 ГБ флешки).
Карты памяти установлены в слоты материнской платы 1A и 2A — по два на каждый «пакет» Operton. Этот документ объясняет, что «CPU» Operton — это иерархическая вещь: package-> die-> module-> core. С помощью этой настройки numactl —hardware сообщает о 4 узлах NUMA, 16 ЦП и 32 ГБ памяти каждый. Я не уверен, что лучше всего вставлять карты памяти в слоты 1A и 2A, но я экспериментирую с ATM.
Я написал тестовую программу на C ++, чтобы помочь мне понять свойства доступа к памяти NUMA
#include <iostream>
#include <numa.h>
#include <pthread.h>
#include <time.h>
#include <omp.h>
#include <cassert>
using namespace std;
const unsigned bufferSize = 50000000;
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()
{
srand(0);
int num_cpus = numa_num_task_cpus();
unsigned* buffers[64] = {0};
for( unsigned whoAllocates = 0; whoAllocates < 64; whoAllocates += 8 )
{
cout << "BUFFERS ARE ALLOCATED BY CORE " << whoAllocates << std::endl;
for( unsigned whichProc = 0; whichProc < 4; ++whichProc )
{
double firstIter1 = 0.0; // The first iterations of cores 0-7 will be summed here
double firstIter2 = 0.0; // for cores 8-15
double allIter1 = 0.0; // all iter cores 0-7
double allIter2 = 0.0; // all iter cores 8-15
#pragma omp parallel
{
assert(omp_get_num_threads() == num_cpus);
int tid = omp_get_thread_num();
pin_to_core( tid );
#pragma omp barrier
if( tid == whoAllocates )
{
for( unsigned i = 0; i < 64; ++i )
{
if( !( i >= 16*whichProc && i < 16 * (whichProc + 1) ) )
continue;
buffers[i] = static_cast<unsigned*>( numa_alloc_local( bufferSize * sizeof(unsigned) ) );
for( unsigned j = 0; j < bufferSize; ++j )
buffers[i][j] = rand();
}
}
#pragma omp barrier
if( tid >= 16*whichProc && tid < 16 * (whichProc + 1) )
{
timespec t1;
clock_gettime( CLOCK_MONOTONIC, &t1 );
unsigned* b = buffers[tid];
unsigned tmp = 0;
unsigned iCur = 0;
double dt = 0.0;
for( unsigned cnt = 0; cnt < 20; ++cnt )
{
for( unsigned j = 0; j < bufferSize/10; ++j )
{
b[iCur] = ( b[iCur] + 13567 ) / 2;
tmp += b[iCur];
iCur = (iCur + 7919) % bufferSize;
}
if( cnt == 0 )
{
timespec t2;
clock_gettime( CLOCK_MONOTONIC, &t2 );
dt = t2.tv_sec - t1.tv_sec + t2.tv_nsec * 0.000000001 - t1.tv_nsec * 0.000000001;
}
}#pragma omp critical
{
timespec t3;
clock_gettime( CLOCK_MONOTONIC, &t3 );
double totaldt = t3.tv_sec - t1.tv_sec + t3.tv_nsec * 0.000000001 - t1.tv_nsec * 0.000000001;
if( (tid % 16) < 8 )
{
firstIter1 += dt;
allIter1 += totaldt;
}
else
{
firstIter2 += dt;
allIter2 += totaldt;
}
}
}
#pragma omp barrier
if( tid == whoAllocates )
{
for( unsigned i = 0; i < 64; ++i )
{
if( i >= 16*whichProc && i < 16 * (whichProc + 1) )
numa_free( buffers[i], bufferSize * sizeof(unsigned) );
}
}
}
cout << firstIter1 / 8.0 << "|" << allIter1 / 8.0 << " / " << firstIter2 / 8.0 << "|" << allIter2 / 8.0 << std::endl;
}
cout << std::endl;
}
return 0;
}
Эта программа распределяет буферы, заполняет их случайными целыми числами и делает с ними несколько бессмысленных вычислений. С помощью итераций цикла мы меняем номер потока / ядра, который выделяет буферы, и номера ядра / потока, которые выполняют работу. Выделение памяти осуществляется по потокам 0,8,16, …, 56. В одно время только 16 потоков делают вычисления, они являются потоками 16i через 16 (г + 1).
Я рассчитываю время, необходимое для выполнения одной единицы работы и выполнения 20 единиц работы. Это сделано, чтобы увидеть изменение скорости, когда некоторые потоки заканчивают выполнение.
Из моих предыдущих экспериментов я заметил, что время доступа к памяти для потоков 8i через 8i + 7 идентичен Так что я просто вывел средние значения времени по 8 сэмплам.
Позвольте мне описать структуру вывода, произведенного моей программой. На самом внешнем уровне находятся блоки, каждый из которых соответствует одному потоку, выполняющему выделение / инициализацию памяти. Каждый такой блок содержит 4 строки, каждая из которых соответствует одному из «пакетов» Operton, выполняющих вычисления (если ядро-распределитель принадлежит текущему «пакету» Operton, то мы ожидаем, что работа будет выполнена быстро). Каждая строка состоит из 2 частей: первая часть соответствует сердечникам 0-7 пакета и 2-я часть соответствует сердечникам 8-15.
Вот вывод:
BUFFERS ARE ALLOCATED BY CORE 0
0.500514|9.9542 / 1.51007|16.5094
2.2603|45.1606 / 2.2775|45.3465
1.68496|28.2412 / 1.08619|21.6404
1.77763|28.9919 / 1.10469|22.1162
BUFFERS ARE ALLOCATED BY CORE 8
0.493291|9.9364 / 1.56316|16.5003
2.26248|45.1783 / 2.27799|45.3355
1.68429|28.25 / 1.08653|21.6459
1.74917|29.0526 / 1.10497|22.1448
BUFFERS ARE ALLOCATED BY CORE 16
1.7351|28.0653 / 1.07199|21.462
0.492752|9.8367 / 1.56163|16.5719
2.24607|44.8697 / 2.27301|45.1844
3.1222|45.1603 / 1.91962|37.9283
BUFFERS ARE ALLOCATED BY CORE 24
1.68059|28.0659 / 1.07882|21.4894
0.492256|9.83806 / 1.56651|16.5694
2.24318|44.9446 / 2.30389|45.1441
3.12939|45.1632 / 1.90041|37.9657
BUFFERS ARE ALLOCATED BY CORE 32
2.2715|45.1583 / 2.2762|45.3947
1.6862|28.1196 / 1.07878|21.561
0.491057|9.82909 / 1.55539|16.5337
3.13294|45.1643 / 1.89497|37.8627
BUFFERS ARE ALLOCATED BY CORE 40
2.26877|45.1215 / 2.28221|45.3919
1.68416|28.1208 / 1.07998|21.5642
0.491796|9.81286 / 1.56934|16.5408
3.12412|45.1824 / 1.91072|37.8004
BUFFERS ARE ALLOCATED BY CORE 48
2.36897|46.8026 / 2.35386|47.0751
3.16056|45.265 / 1.89596|38.0117
3.14169|45.1464 / 1.89043|37.8944
0.493718|9.84713 / 1.56139|16.5472
BUFFERS ARE ALLOCATED BY CORE 56
2.35647|46.823 / 2.36314|47.0848
3.12441|45.2807 / 1.90549|38.0006
3.12573|45.1325 / 1.89693|37.8699
0.491999|9.83378 / 1.56538|16.5302
Например, четвертая строка в блоке, соответствующая выделению ядром № 16, — это «3,1222 | 45,1603 / 1,91962 | 37,9283». Это означает, что в среднем потребовались ядра 48-55 3.1222 для выполнения первой единицы работы и 45.1603 для выполнения всех 20 единиц работы (это не в 20 раз больше, поскольку очевидно, что после завершения работы ядер 56-63 наблюдается ускорение ). Вторая половина строки говорит нам, что в среднем потребовалось 56–63 ядра 1.91962 для выполнения первой итерации и 37.9283 для выполнения всех 20 итераций.
Вещи, которые я не могу понять:
Может ли кто-нибудь дать понять, почему это происходит?
Задача ещё не решена.
Других решений пока нет …