问题
我在蒙特卡洛粒子模拟中遇到了内存消耗问题,我在其中使用 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:我已更改
volatile为std::atomic - 回复@Gilles:我已经检查了 16 个线程案例的“粒子移动”并相应更新
最小的例子
我终于能够在一个最小的例子中重现这个问题,它最终变得非常简单,这里的所有细节都是不必要的。我在这里创建了一个没有乱七八糟的新问题。