1

问题

我在蒙特卡洛粒子模拟中遇到了内存消耗问题,我在其中使用 OpenMP 进行并行化。不讨论模拟方法的细节,一个并行部分是使用一些线程的“粒子移动”,另一个是使用一些可能不同数量的线程的“缩放移动”。这 2 个并行代码由一些串行内核可互换地运行,每个代码都需要几毫秒才能运行。

我有一台运行 Linux Ubuntu 18.04 LTS 的 8 核 16 线程机器,我正在使用 gcc 和 GNU OpenMP 实现。现在:

  • 为“粒子移动”使用8 个线程为“缩放移动”使用 8 个线程可产生稳定的 8-9 MB 内存使用
  • 使用8 个线程进行“粒子移动”和16 个线程进行“缩放移动”会导致内存消耗从 8 MB 增加到数十 GB 以进行长时间模拟,最终导致 OOM 终止
  • 使用16线程16线程就可以
  • 使用16线程8线程导致消耗增加

因此,如果这两种移动的线程数不匹配,就会出现问题。

不幸的是,我无法在一个最小的示例中重现该问题,我只能给出 OpenMP 代码的摘要。一个最小示例的链接在底部。

在模拟中,我有 N 个具有某些位置的粒子。“粒子移动”组织在一个网格中,我collapse(3)用来分配线程。代码看起来或多或少是这样的:

// Each threads has its own cell in a 2 x 2 x 2 grid
#pragma omp parallel for collapse(3) num_threads(8 or 16)
for (std::size_t i = 0; i < 2; i++) {
    for (std::size_t j = 0; j < 2; j++) {
        for (std::size_t k = 0; k < 2; k++) {
            std::array<std::size_t, 3> gridCoords = {i, j, k};
            
            // This does something for all particles in {i, j, k} grid cell
            doIndependentParticleMovesInAGridCellGivenByCoords(gridCoords);
        }
    }
}

(注意,在这两种情况下都只分配了 8 个线程 - 8 个和 16 个,但是使用那些额外的、无工作的 8 个线程可以神奇地解决使用 16 个缩放线程时的问题。)

在“体积移动”中,我独立地对每个粒子进行重叠检查,并在发现第一个重叠时退出。它看起来像这样:

// We independently check for each particle
std::atomic<bool> overlapFound = false;
#pragma omp parallel for num_threads(8 or 16)
for (std::size_t i = 0; i < N; i++) {
    if (overlapFound)
        continue;
    if (isParticleOverlappingAnything(i))
        overlapFound = true;
}

现在,在并行区域中,我不分配任何新内存,也不需要任何临界区——不应该有竞争条件。

此外,整个程序中的所有内存管理都是由 std::vector、std::unique_ptr 等以 RAII 方式完成的 - 我不使用newordelete任何地方

调查

我尝试使用一些 Valgrind 工具。我运行了一段时间的模拟,对于不匹配的线程数情况,它会产生大约 16 MB(仍在增加)的内存消耗,而对于匹配的情况,它仍然保持在 8 MB 。

  • 在任何一种情况下,Valgrind Memcheck 都不会显示任何内存泄漏(OpenMP 控制结构中只有几 kB“仍可访问”或“可能丢失”,请参见此处)。
  • Valgrind Massif 在这两种情况下都只报告那些“正确”的 8 MB 分配内存。

我还尝试将 main in 的内容包围起来{ }并添加while(true)

int main() {
    {
        // Do the simulation and let RAII do all the cleanup when destructors are called
    }

    // Hang
    while(true) { }
}

在模拟过程中,内存消耗会增加到 100 MB。结束{ ... }执行时,内存消耗降低了大约 6 MB 并保持在 94 英寸while(true)- 6 MB 是最大数据结构的实际大小(我估计它),但剩余部分是未知的。

假设

所以我认为它必须与 OpenMP 内存管理有关。也许交替使用 8 和 16 线程会导致 OpenMP 不断创建新线程池而放弃旧线程池而不释放资源?我在这里找到了类似的东西,但它似乎是另一个 OpenMP 实现。

我将非常感谢一些想法,我还能检查什么以及问题可能出在哪里。

  • 回复@1201ProgramAlarm:我已更改volatilestd::atomic
  • 回复@Gilles:我已经检查了 16 个线程案例的“粒子移动”并相应更新

最小的例子

我终于能够在一个最小的例子中重现这个问题,它最终变得非常简单,这里的所有细节都是不必要的。我在这里创建了一个没有乱七八糟的新问题。

4

1 回答 1

1

问题出在哪里?

问题似乎与此特定代码的作用或 OpenMP 子句的结构无关,而仅与具有不同线程数的两个交替 OpenMP 并行区域有关。在进行了数百万次更改之后,无论这些部分中的内容如何,​​进程都会使用大量内存。它们甚至可能像睡几毫秒一样简单。

由于这个问题包含太多不必要的细节,我将讨论转移到更直接的问题这里。我推荐有兴趣的读者。

简要总结发生的事情

在这里,我简要总结了 StackOverflow 成员和我能够确定的内容。假设我们有 2 个具有不同线程数的 OpenMP 部分,例如这里:

#include <unistd.h>

int main() {
    while (true) {
        #pragma omp parallel num_threads(16)
        usleep(30);

        #pragma omp parallel num_threads(8)
        usleep(30);
    }
    return 0;
}

正如此处详细描述的那样,OpenMP 重用了常用的 8 个线程,但 16 线程部分所需的其他 8 个线程会不断创建和销毁。这种持续的线程创建会导致内存消耗增加,我不知道是因为实际的内存泄漏还是内存碎片。此外,该问题似乎特定于 GCC 中的 GOMP OpenMP 实现(至少到版本 10)。Clang 和 Intel 编译器似乎没有复制这个问题。

尽管 OpenMP 标准没有明确说明,但大多数实现倾向于重用已经产生的线程,但 GOMP 似乎并非如此,这可能是一个错误。我将提交错误问题并更新答案。目前,唯一的解决方法是在每个并行区域中使用相同数量的线程(然后 GOMP 正确地重用旧线程)。在像collapse问题中的循环这样的情况下,当要分配的线程比其他部分少时,总是可以请求 16 个线程而不是 8 个,而让其他 8 个什么也不做。它在我的“生产”代码中运行良好。

于 2021-04-29T18:01:20.207 回答