2

我是 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 个线程在进行计算,它们是线程16i16(i+1)

我正在计算完成一个工作单元和完成 20 个工作单元所需的时间。这样做是为了查看一些线程完成执行时速度的变化。

从我之前的实验中,我注意到线程8i8i+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 秒。

我无法理解的事情:

  1. 例如,当分配完成时,在线程 8 上,线程 0-7 仍然在线程 8-15 之前完成工作。我希望执行分配和初始化的线程至少不晚于所有其他线程完成。
  2. 四个 Operton 软件包之间存在一些不对称性。例如,平均访问 package1 的内存(由内核 0 或 8 分配时)比访问 package4 的内存(由内核 48 或 56 分配)更快。

任何人都可以对为什么会发生这种情况提供任何见解吗?

4

0 回答 0