是什么导致基于 OpenMP 的仿真中内存消耗增加?
What causes increasing memory consumption in OpenMP-based simulation?
问题
我在使用 OpenMP 进行并行化的 Monte Carlo 粒子模拟中遇到内存消耗问题。不深入讨论模拟方法的细节,一个并行部分是使用一定数量的线程的“粒子移动”,另一个是使用一些可能不同数量的线程的“缩放移动”。这 2 个并行代码 运行 由一些串行核心可互换地分隔开,每个都需要几毫秒才能达到 运行。
我有一台 8 核 16 线程机器 运行ning Linux Ubuntu 18.04 LTS,我正在使用 gcc 和 GNU OpenMP 实现。现在:
- 使用 8 个线程 进行“粒子移动”,使用 8 个线程 进行“缩放移动”产生 稳定的 8- 9 MB 内存使用量
- 使用 8 个线程 进行“粒子移动”,使用 16 个线程 进行“缩放移动”会导致 内存消耗增加从 8 MB 到数十 GB 的长时间模拟导致 OOM kill
- 使用 16 个线程 和 16 个线程 可以
- 使用 16 个线程 和 8 个线程 会导致 消耗增加
因此,如果这两种类型的移动的线程数不匹配,就会出现问题。
不幸的是,我无法在最小示例中重现该问题,我只能提供 OpenMP 代码的摘要。 link 最简单的例子在底部。
在模拟中,我有 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 方式完成的。 - 我不使用 new
或 delete
任何地方.
调查
我尝试使用一些 Valgrind 工具。我 运行 模拟了一段时间,在 非匹配 线程数情况下产生大约 16 MB 的(仍在增加)内存消耗,而它仍然保持在 8 MB 上匹配 个案例。
- 在任何一种情况下,Valgrind Memcheck 都没有显示任何内存泄漏(OpenMP 控制结构中只有几 kB“仍然可以访问”或“可能丢失”,请参阅 here)。
- Valgrind Massif 在这两种情况下仅报告那些“正确”的 8 MB 分配内存。
我也试过把{ }
中main的内容包围起来,加上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,并在 while(true)
中保持在 94 - 6 MB 是最大数据结构的实际大小(我估计的),但剩余部分是未知种类。
假设
所以我认为它一定与 OpenMP 内存管理有关。也许交替使用 8 个和 16 个线程会导致 OpenMP 在不释放资源的情况下不断创建新线程池而放弃旧线程池?我发现了类似这样的东西 here,但它似乎是另一个 OpenMP 实现。
我将非常感谢您提供一些想法我还可以检查什么以及可能的问题所在。
- 回复@1201ProgramAlarm:我已经把
volatile
改成了std::atomic
- 回复@Gilles:我已经检查了 16 个线程的“粒子移动”情况并进行了相应更新
最小示例
我终于能够在一个最小的例子中重现这个问题,它最终变得非常简单,这里的所有细节都是不必要的。我创建了一个没有所有混乱的新问题 here.
问题出在哪里?
问题似乎与此特定代码的作用或 OpenMP 子句的结构无关,而仅与两个具有不同线程数的交替 OpenMP 并行区域有关。在数百万次这样的更改之后,无论这些部分中有什么,进程都会使用大量内存。它们甚至可能像睡几毫秒一样简单。
由于这个问题包含太多不必要的细节,我已将讨论转移到更直接的问题here。我推荐感兴趣的 reader.
所发生情况的简要总结
在这里,我简要总结了 Whosebug 成员和我能够确定的内容。假设我们有 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 个线程什么也不做。它在我的“生产”代码中运行得很好。
问题
我在使用 OpenMP 进行并行化的 Monte Carlo 粒子模拟中遇到内存消耗问题。不深入讨论模拟方法的细节,一个并行部分是使用一定数量的线程的“粒子移动”,另一个是使用一些可能不同数量的线程的“缩放移动”。这 2 个并行代码 运行 由一些串行核心可互换地分隔开,每个都需要几毫秒才能达到 运行。
我有一台 8 核 16 线程机器 运行ning Linux Ubuntu 18.04 LTS,我正在使用 gcc 和 GNU OpenMP 实现。现在:
- 使用 8 个线程 进行“粒子移动”,使用 8 个线程 进行“缩放移动”产生 稳定的 8- 9 MB 内存使用量
- 使用 8 个线程 进行“粒子移动”,使用 16 个线程 进行“缩放移动”会导致 内存消耗增加从 8 MB 到数十 GB 的长时间模拟导致 OOM kill
- 使用 16 个线程 和 16 个线程 可以
- 使用 16 个线程 和 8 个线程 会导致 消耗增加
因此,如果这两种类型的移动的线程数不匹配,就会出现问题。
不幸的是,我无法在最小示例中重现该问题,我只能提供 OpenMP 代码的摘要。 link 最简单的例子在底部。
在模拟中,我有 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 方式完成的。 - 我不使用 new
或 delete
任何地方.
调查
我尝试使用一些 Valgrind 工具。我 运行 模拟了一段时间,在 非匹配 线程数情况下产生大约 16 MB 的(仍在增加)内存消耗,而它仍然保持在 8 MB 上匹配 个案例。
- 在任何一种情况下,Valgrind Memcheck 都没有显示任何内存泄漏(OpenMP 控制结构中只有几 kB“仍然可以访问”或“可能丢失”,请参阅 here)。
- Valgrind Massif 在这两种情况下仅报告那些“正确”的 8 MB 分配内存。
我也试过把{ }
中main的内容包围起来,加上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,并在 while(true)
中保持在 94 - 6 MB 是最大数据结构的实际大小(我估计的),但剩余部分是未知种类。
假设
所以我认为它一定与 OpenMP 内存管理有关。也许交替使用 8 个和 16 个线程会导致 OpenMP 在不释放资源的情况下不断创建新线程池而放弃旧线程池?我发现了类似这样的东西 here,但它似乎是另一个 OpenMP 实现。
我将非常感谢您提供一些想法我还可以检查什么以及可能的问题所在。
- 回复@1201ProgramAlarm:我已经把
volatile
改成了std::atomic
- 回复@Gilles:我已经检查了 16 个线程的“粒子移动”情况并进行了相应更新
最小示例
我终于能够在一个最小的例子中重现这个问题,它最终变得非常简单,这里的所有细节都是不必要的。我创建了一个没有所有混乱的新问题 here.
问题出在哪里?
问题似乎与此特定代码的作用或 OpenMP 子句的结构无关,而仅与两个具有不同线程数的交替 OpenMP 并行区域有关。在数百万次这样的更改之后,无论这些部分中有什么,进程都会使用大量内存。它们甚至可能像睡几毫秒一样简单。
由于这个问题包含太多不必要的细节,我已将讨论转移到更直接的问题here。我推荐感兴趣的 reader.
所发生情况的简要总结
在这里,我简要总结了 Whosebug 成员和我能够确定的内容。假设我们有 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 标准没有明确说明,但大多数实现倾向于重用已经生成的线程,但 GOMP 似乎并非如此,这可能是一个错误。我将提交错误问题并更新答案。目前,唯一的解决方法是在每个并行区域使用相同数量的线程(然后 GOMP 正确地重用旧线程)。在问题的 collapse
循环的情况下,当要分配的线程比另一部分少时,总是可以请求 16 个线程而不是 8 个线程,而让其他 8 个线程什么也不做。它在我的“生产”代码中运行得很好。