我有一个真正的谜题给你们。
下面是一个小型的、独立的、简单的 40 行程序,它计算一堆数字的部分总和,并经常(但随机地)使我正在使用的分布式内存集群上的节点崩溃。如果我生成 50 个运行此代码的 PBS 作业,其中 0 到 4 个将使其节点崩溃。每次都会在主循环的不同重复和不同节点上发生,没有可辨别的模式。节点只是在神经节报告中“关闭”,我无法通过 ssh 连接到它们(“没有到主机的路由”)。如果我没有提交作业,而是 ssh 到其中一个节点上并在那里运行我的程序,如果我不走运并且它崩溃了,那么我就不再看到文本,然后看到那个节点在神经节上已经死了。
该程序使用 openmp 进行线程化,并且仅在产生大量线程(例如 12 个)时才会发生崩溃。
它杀死的集群是一个 RHEL 5 集群,其节点具有 2 个 6 核 x5650 处理器:
[jamelang@hooke ~]$ tail /etc/redhat-release
Red Hat Enterprise Linux Server release 5.7 (Tikanga)
我尝试启用核心转储ulimit -c unlimited
,但没有文件显示。这是代码,带有注释:
#include <cstdlib>
#include <cstdio>
#include <omp.h>
int main() {
const unsigned int numberOfThreads = 12;
const unsigned int numberOfPartialSums = 30000;
const unsigned int numbersPerPartialSum = 40;
// make some numbers
srand(0); // every instance of program should get same results
const unsigned int totalNumbersToSum = numbersPerPartialSum * numberOfPartialSums;
double * inputData = new double[totalNumbersToSum];
for (unsigned int index = 0; index < totalNumbersToSum; ++index) {
inputData[index] = rand()/double(RAND_MAX);
}
omp_set_num_threads(numberOfThreads);
// prepare a place to dump output
double * partialSums = new double[numberOfPartialSums];
// do the following algorithm many times to induce a problem
for (unsigned int repeatIndex = 0; repeatIndex < 100000; ++repeatIndex) {
if (repeatIndex % 1000 == 0) {
printf("Absurd testing is on repeat %06u\n", repeatIndex);
}
#pragma omp parallel for
for (unsigned int partialSumIndex = 0; partialSumIndex < numberOfPartialSums;
++partialSumIndex) {
// get this partial sum's limits
const unsigned int beginIndex = numbersPerPartialSum * partialSumIndex;
const unsigned int endIndex = numbersPerPartialSum * (partialSumIndex + 1);
// we just sum the 40 numbers, can't get much simpler
double sumOfNumbers = 0;
for (unsigned int index = beginIndex; index < endIndex; ++index) {
// only reading, thread-safe
sumOfNumbers += inputData[index];
}
// writing to non-overlapping indices (guaranteed by omp),
// should be thread-safe.
// at worst we would have false sharing, but that would just affect
// performance, not throw sigabrts.
partialSums[partialSumIndex] = sumOfNumbers;
}
}
delete[] inputData;
delete[] partialSums;
return 0;
}
我用以下内容编译它:
/home/jamelang/gcc-4.8.1/bin/g++ -O3 -Wall -fopenmp Killer.cc -o Killer
它似乎链接到正确的共享对象:
[jamelang@hooke Killer]$ ldd Killer
linux-vdso.so.1 => (0x00007fffc0599000)
libstdc++.so.6 => /home/jamelang/gcc-4.8.1/lib64/libstdc++.so.6 (0x00002b155b636000)
libm.so.6 => /lib64/libm.so.6 (0x0000003293600000)
libgomp.so.1 => /home/jamelang/gcc-4.8.1/lib64/libgomp.so.1 (0x00002b155b983000)
libgcc_s.so.1 => /home/jamelang/gcc-4.8.1/lib64/libgcc_s.so.1 (0x00002b155bb92000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x0000003293a00000)
libc.so.6 => /lib64/libc.so.6 (0x0000003292e00000)
/lib64/ld-linux-x86-64.so.2 (0x0000003292a00000)
librt.so.1 => /lib64/librt.so.1 (0x0000003298600000)
一些注意事项:
1. 在使用 gcc 4.7 的 osx lion 上,此代码将抛出 SIGABRT,类似于以下问题:Why is this code given SIGABRT with openMP?. 使用 gcc 4.8 似乎可以解决 OSX 上的问题。但是,在 RHEL5 机器上使用 gcc 4.8 并不能修复它。RHEL5 机器有 GLIBC 版本 2.5,而且 yum 似乎没有提供更高版本,所以管理员坚持使用 2.5。
2. 如果我定义了一个 SIGABRT 信号处理程序,它不会在 RHEL5 机器上发现问题,但在 OSX 上使用 gcc47 会发现问题。
3. 我认为不需要在 omp 子句中共享任何变量,因为它们都可以拥有私有副本,但是将它们添加为共享并不会改变行为。
4. 无论使用的优化级别如何,都会发生节点的杀戮。
5. 即使我从 gdb 中运行程序(即在 pbs 文件中放入“gdb -batch -x gdbCommands Killer”),也会发生节点的终止,其中“gdbCommands”是一个包含一行的文件:“run”
6. 这示例在每次重复时产生线程。一种策略是制作一个包含重复循环的并行块,以防止这种情况发生。然而,这对我没有帮助——这个例子只代表一个更大的研究代码,我不能在其中使用该策略。
我完全没有想法,在我的最后一根稻草上,在我的智慧尽头,准备把我的头发拉出来,等等。有没有人有建议或想法?