在 ccNUMA 系统上测量带宽

Measuring bandwidth on a ccNUMA system

我正在尝试使用 2x Intel(R) Xeon(R) Platinum 8168 对 ccNUMA 系统的内存带宽进行基准测试:

  1. 24 核 @ 2.70 GHz,
  2. 一级缓存 32 kB,二级缓存 1 MB 和三级缓存 33 MB。

作为参考,我使用了 Intel Advisor 的 Roofline 图,它描述了每个 CPU 可用数据路径的带宽。据此,带宽为230GB/s.

为了对此进行基准测试,我使用了我自己的小基准测试助手工具,它在循环中执行定时实验。 API 提供了一个名为 experiment_functor 的摘要 class 看起来像这样:

class experiment_functor
{
public:
    
    //+/////////////////
    // main functionality
    //+/////////////////
    
    virtual void init() = 0;
    
    virtual void* data(const std::size_t&) = 0;
    
    virtual void perform_experiment() = 0;
    
    virtual void finish() = 0;
};

然后用户(我自己)可以定义数据初始化、要计时的工作,即实验和清理例程,以便新分配的数据可以用于每个实验。派生的 class 的实例可以提供给 API 函数:

perf_stats perform_experiments(experiment_functor& exp_fn, const std::size_t& data_size_in_byte, const std::size_t& exp_count)

这是 Schönauer 向量三元组 class 的实现:

class exp_fn : public experiment_functor
{
    //+/////////////////
    // members
    //+/////////////////
    
    const std::size_t data_size_;
    double* vec_a_ = nullptr;
    double* vec_b_ = nullptr;
    double* vec_c_ = nullptr;
    double* vec_d_ = nullptr;
    
public:
    
    //+/////////////////
    // lifecycle
    //+/////////////////
    
    exp_fn(const std::size_t& data_size)
        : data_size_(data_size) {}
    
    //+/////////////////
    // main functionality
    //+/////////////////
    
    void init() final
    {
        // allocate
        const auto page_size = sysconf(_SC_PAGESIZE) / sizeof(double);
        posix_memalign(reinterpret_cast<void**>(&vec_a_), page_size, data_size_ * sizeof(double));
        posix_memalign(reinterpret_cast<void**>(&vec_b_), page_size, data_size_ * sizeof(double));
        posix_memalign(reinterpret_cast<void**>(&vec_c_), page_size, data_size_ * sizeof(double));
        posix_memalign(reinterpret_cast<void**>(&vec_d_), page_size, data_size_ * sizeof(double));
        if (vec_a_ == nullptr || vec_b_ == nullptr || vec_c_ == nullptr || vec_d_ == nullptr)
        {
            std::cerr << "Fatal error, failed to allocate memory." << std::endl;
            std::abort();
        }

        // apply first-touch
        #pragma omp parallel for schedule(static)
        for (auto index = std::size_t{}; index < data_size_; index += page_size)
        {
            vec_a_[index] = 0.0;
            vec_b_[index] = 0.0;
            vec_c_[index] = 0.0;
            vec_d_[index] = 0.0;
        }
    }
    
    void* data(const std::size_t&) final
    {
        return reinterpret_cast<void*>(vec_d_);
    }
    
    void perform_experiment() final
    {
        #pragma omp parallel for simd safelen(8) schedule(static)
        for (auto index = std::size_t{}; index < data_size_; ++index)
        {
            vec_d_[index] = vec_a_[index] + vec_b_[index] * vec_c_[index]; // fp_count: 2, traffic: 4+1
        }
    }
    
    void finish() final
    {
        std::free(vec_a_);
        std::free(vec_b_);
        std::free(vec_c_);
        std::free(vec_d_);
    }
};

注意: 函数 data 有一个特殊用途,它试图抵消 NUMA 平衡的影响。通常,在随机迭代中,函数 perform_experiments 使用所有线程以随机方式写入此函数提供的数据。

问题: 使用这个我一直得到最大值。 201 GB/s 的带宽。为什么我无法达到规定的 230 GB/s?

如果需要,我很乐意提供任何额外信息。非常感谢您的回答。


更新:

根据@VictorEijkhout 的建议,我现在对只读带宽进行了强大的扩展实验。

如您所见,峰值带宽确实是平均 217 GB/s,最大 225 GB/s。仍然很费解的是,在某一点上,增加CPUs实际上减少了有效带宽。

带宽性能取决于您执行的操作类型。对于混合读写,您确实不会获得峰值;如果你只做阅读,你会离得更近。

我建议您阅读“Stream 基准测试”的文档,并查看发布的数字。

进一步说明:我希望你把你的线程与 OMP_PROC_BIND 联系起来?此外,您的架构在用完内核之前就用完了带宽。您的最佳带宽性能可能会在少于内核总数的情况下发生。