これは少し遅いですが、うまくいけば、このサンプルコードが同様の立場にいる他の人を助けるでしょう!
osgxが述べたように、OpenMPはシグナルの問題について沈黙していますが、OpenMPはPOSIXシステム上のpthreadで実装されることが多いため、pthreadシグナルアプローチを使用できます。
OpenMPを使用した大量の計算の場合、計算を実際に安全に停止できる場所はごくわずかである可能性があります。したがって、時期尚早の結果を取得したい場合は、同期信号処理を使用してこれを安全に行うことができます。追加の利点は、これにより特定のOpenMPスレッドからのシグナルを受け入れることができることです(以下のサンプルコードでは、マスタースレッドを選択します)。信号をキャッチすると、計算を停止する必要があることを示すフラグを設定するだけです。次に、各スレッドは、都合のよいときにこのフラグを定期的にチェックしてから、ワークロードのシェアをまとめる必要があります。
この同期アプローチを使用することにより、アルゴリズムへの変更を最小限に抑えて、計算を正常に終了できます。一方、各スレッドの現在の動作状態をコヒーレントな結果に照合することは困難である可能性があるため、必要に応じたシグナルハンドラーアプローチは適切でない場合があります。ただし、同期アプローチの欠点の1つは、計算が停止するまでにかなりの時間がかかる可能性があることです。
信号チェック装置は、次の3つの部分で構成されています。
- 関連する信号をブロックします。
omp parallel
これは、各OpenMPスレッド(pthread)がこれと同じブロッキング動作を継承するように、リージョン外で実行する必要があります。
- マスタースレッドからの目的のシグナルをポーリングします。これに使用できます
sigtimedwait
が、一部のシステム(MacOSなど)はこれをサポートしていません。より移植性の高い方法sigpending
として、ブロックされた信号をポーリングし、ブロックされた信号が期待どおりであることを再確認してから、同期を使用して受け入れることができますsigwait
(プログラムの他の部分が競合状態を作成していない限り、ここにすぐに戻るはずです)。 )。最終的に関連するフラグを設定しました。
- 最後にシグナルマスクを削除する必要があります(オプションで、シグナルの最終チェックを1回行います)。
パフォーマンスに関する重要な考慮事項と注意事項がいくつかあります。
- 各内部ループの反復が小さいと仮定すると、シグナルチェックシステムコールの実行にはコストがかかります。サンプルコードでは、1000万(スレッドあたり)の反復ごとにのみシグナルをチェックします。これは、おそらく数秒の壁時間に相当します。
omp for
ループを1から分割することはできないため、残りの反復でスピンするか、より基本的なOpenMPプリミティブを使用してループを書き直す必要があります。通常のループ(外側の並列ループの内側のループなど)は、問題なく分割できます。
- マスタースレッドのみがシグナルをチェックできる場合、マスタースレッドが他のスレッドよりもかなり前に終了するプログラムで問題が発生する可能性があります。このシナリオでは、これらの他のスレッドは中断できません。これに対処するには、各スレッドがワークロードを完了するときにシグナルチェックの「バトンを渡す」か、他のすべてのスレッドが完了するまでマスタースレッドの実行とポーリングを強制することができます2。
- NUMA HPCなどの一部のアーキテクチャでは、「グローバル」信号フラグをチェックする時間が非常に長くなる可能性があるため、フラグをチェックまたは操作するタイミングと場所を決定するときは注意してください。たとえば、スピンループセクションの場合、フラグがtrueになったときにローカルにキャッシュしたい場合があります。
サンプルコードは次のとおりです。
#include <signal.h>
void calculate() {
_Bool signalled = false;
int sigcaught;
size_t steps_tot = 0;
// block signals of interest (SIGINT and SIGTERM here)
sigset_t oldmask, newmask, sigpend;
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
sigaddset(&newmask, SIGTERM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
#pragma omp parallel
{
int rank = omp_get_thread_num();
size_t steps = 0;
// keep improving result forever, unless signalled
while (!signalled) {
#pragma omp for
for (size_t i = 0; i < 10000; i++) {
// we can't break from an omp for loop...
// instead, spin away the rest of the iterations
if (signalled) continue;
for (size_t j = 0; j < 1000000; j++, steps++) {
// ***
// heavy computation...
// ***
// check for signal every 10 million steps
if (steps % 10000000 == 0) {
// master thread; poll for signal
if (rank == 0) {
sigpending(&sigpend);
if (sigismember(&sigpend, SIGINT) || sigismember(&sigpend, SIGTERM)) {
if (sigwait(&newmask, &sigcaught) == 0) {
printf("Interrupted by %d...\n", sigcaught);
signalled = true;
}
}
}
// all threads; stop computing
if (signalled) break;
}
}
}
}
#pragma omp atomic
steps_tot += steps;
}
printf("The result is ... after %zu steps\n", steps_tot);
// optional cleanup
sigprocmask(SIG_SETMASK, &oldmask, NULL);
}
C ++を使用している場合は、次のクラスが役立つ場合があります...
#include <signal.h>
#include <vector>
class Unterminable {
sigset_t oldmask, newmask;
std::vector<int> signals;
public:
Unterminable(std::vector<int> signals) : signals(signals) {
sigemptyset(&newmask);
for (int signal : signals)
sigaddset(&newmask, signal);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
}
Unterminable() : Unterminable({SIGINT, SIGTERM}) {}
// this can be made more efficient by using sigandset,
// but sigandset is not particularly portable
int poll() {
sigset_t sigpend;
sigpending(&sigpend);
for (int signal : signals) {
if (sigismember(&sigpend, signal)) {
int sigret;
if (sigwait(&newmask, &sigret) == 0)
return sigret;
break;
}
}
return -1;
}
~Unterminable() {
sigprocmask(SIG_SETMASK, &oldmask, NULL);
}
};
次に、のブロッキング部分をcalculate()
、に置き換えUnterminable unterm();
、信号チェック部分を。に置き換えることができif ((sigcaught = unterm.poll()) > 0) {...}
ます。信号のブロック解除はunterm
、スコープ外になると自動的に実行されます。
1これは厳密には真実ではありません。OpenMPは、キャンセルポイントの形式で「並列ブレーク」を実行するための限定的なサポートをサポートします。並列ループでキャンセルポイントを使用する場合は、キャンセル時に計算データの一貫性を確保するために、暗黙のキャンセルポイントがどこにあるかを正確に把握してください。
2個人的には、forループを完了したスレッドの数をカウントし、マスタースレッドがシグナルをキャッチせずにループを完了した場合、シグナルをキャッチするか、すべてのスレッドがループを完了するまで、シグナルのポーリングを続けます。これを行うには、必ずforループをマークしてくださいnowait
。