我是 NUMA 主题的新手。我还需要说我是一名程序员,对硬件没有深入的了解。
我正在使用 Quad Operton 6272 服务器。主板是SuperMicro H8QGi+-F,总共有 132GB 内存(8 个16GB 棒)。
记忆棒安装在主板插槽 1A 和 2A 中 - 每个 Operton“包装”两个。本文档解释了一个 Operton “CPU” 是一个分层的东西:package->die->module->core。使用此设置,'numactl --hardware' 报告 4 个 NUMA 节点、16 个 CPU 和 32GB 内存。我不确定将记忆棒放入插槽 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(i+1)。
我正在计算完成一个工作单元和完成 20 个工作单元所需的时间。这样做是为了查看一些线程完成执行时速度的变化。
从我之前的实验中,我注意到线程8i到8i+7的内存访问时间是相同的。所以我只是输出8个样本的平均时间。
让我描述一下我的程序产生的输出结构。在最外层有块,每个对应一个线程进行内存分配/初始化。每个这样的块包含 4 行,每行对应于一个进行计算的 Operton“包”(如果分配的核心属于当前的 Operton“包”,那么我们预计工作会很快完成)。每行由两部分组成:第一部分对应于封装的核心 0-7,第二部分对应于核心 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
例如,对应于 core #16 分配的块中的第四行是“3.1222|45.1603 / 1.91962|37.9283”。这意味着平均而言,核心 48-55 3.1222s 完成第一个工作单元,45.1603s 完成所有 20 个工作单元(不是 20 倍,因为当核心 56-63 完成时显然有加速)。该行的后半部分告诉我们,平均而言,完成第一次迭代需要 56-63 个内核 1.91962 秒,完成所有 20 次迭代需要 37.9283 秒。
我无法理解的事情:
- 例如,当分配完成时,在线程 8 上,线程 0-7 仍然在线程 8-15 之前完成工作。我希望执行分配和初始化的线程至少不晚于所有其他线程完成。
- 四个 Operton 软件包之间存在一些不对称性。例如,平均访问 package1 的内存(由内核 0 或 8 分配时)比访问 package4 的内存(由内核 48 或 56 分配)更快。
任何人都可以对为什么会发生这种情况提供任何见解吗?