5

OpenMP 内のタスクを使用して並列アルゴリズムを実装しようとしています。並列プログラミング パターンはプロデューサー/コンシューマーの考え方に基づいていますが、コンシューマー プロセスはプロデューサーよりも遅いため、少数のプロデューサーと複数のコンシューマーを使用したいと考えています。主なアイデアは、プロデューサーと同じ数の OS スレッドを作成し、これらのそれぞれが (コンシューマーによって) 並行して実行されるタスクを作成することです。すべてのプロデューサは、比例した数のコンシューマ (つまり、numCheckers/numSeekers) に関連付けられます。チップあたり 6 コアのインテル デュアルチップ サーバーでアルゴリズムを実行しています。問題は、1 つのプロデューサー (シーカー) と増加する数のコンシューマー (チェッカー) のみを使用すると、正しい数のコアが動作しているにもかかわらず、コンシューマーの数が増えるにつれてパフォーマンスが非常に速く低下することです (下の表を参照)。 100%。一方で、プロデューサーの数を増やすと、コンシューマーの数が比例していても、平均時間が減少するか、少なくとも安定したままになります。すべての改善は生産者間の入力の分割によって行われ、タスクはバグであると私には思えます。しかし、繰り返しになりますが、あるプロデューサーの振る舞いについては説明がありません。OpenMP-Task ロジックで何か不足していますか? 私は何か間違ったことをしていますか?

-------------------------------------------------------------------------
|   producers   |   consumers   |   time        |
-------------------------------------------------------------------------
|       1       |       1       |   0.642935    |
|       1       |       2       |   3.004023    |
|       1       |       3       |   5.332524    |
|       1       |       4       |   7.222009    |
|       1       |       5       |   9.472093    |
|       1       |       6       |   10.372389   |
|       1       |       7       |   12.671839   |
|       1       |       8       |   14.631013   |
|       1       |       9       |   14.500603   |
|       1       |      10       |   18.034931   |
|       1       |      11       |   17.835978   |
-------------------------------------------------------------------------
|       2       |       2       |   0.357881    |
|       2       |       4       |   0.361383    |
|       2       |       6       |   0.362556    |
|       2       |       8       |   0.359722    |
|       2       |      10       |   0.358816    |
-------------------------------------------------------------------------

私のコードの主要なセクションは次のとおりです。

int main( int argc, char** argv) {

  // ... process the input (read from file, etc...)

  const char *buffer_start[numSeekers];
  int buffer_len[numSeekers];

  //populate these arrays dividing the input
  //I need to do this because I need to overlap the buffers for
  //correctness, so I simple parallel-for it's not enough 

  //Here is where I create the producers
  int num = 0;
  #pragma omp parallel for num_threads(numSeekers) reduction(+:num)
  for (int i = 0; i < numSeekers; i++) {
      num += seek(buffer_start[i], buffer_len[i]);
  }

  return (int*)num;
}

int seek(const char* buffer, int n){

  int num = 0;

  //asign the same number of consumers for each producer 
  #pragma omp parallel num_threads(numCheckers/numSeekers) shared(num)
  {
    //only one time for every producer
    #pragma omp single
    {
      for(int pos = 0; pos < n; pos += STEP){
    if (condition(buffer[pos])){
      #pragma omp task shared(num)
      {
        //check() is a sequential function
        num += check(buffer[pos]);
      }
    }
      }
      #pragma omp taskwait
    }
  return num;
}
4

2 に答える 2

5

parallel観察された動作は、ネストされた領域が有効になっていないという事実によるものです。何が起こるかというと、最初のケースでは、OpenMPタスクの巨大なオーバーヘッドが実際に発生しているということです。check()これは、OpenMPランタイムがもたらすオーバーヘッドと比較して十分な作業を行っていないという事実が原因である可能性が最も高いです。なぜ1人と2人のプロデューサーでそのように動作するのですか?

1つのプロデューサーparallelのみで実行している場合、外部領域は1つのスレッドのみで実行されます。このようなparallel領域は、OpenMP API仕様に従って非アクティブであり、コードをシリアルに実行するだけです(唯一のオーバーヘッドは、追加の関数呼び出しとポインターを介した共有変数へのアクセスです)。この場合、内部parallel領域は、ネストされた並列処理が無効になっているときにネストされますが、アクティブになり、多くのタスクに拍車をかけます。タスクは比較的高いオーバーヘッドをもたらし、このオーバーヘッドはスレッドの数とともに増加します。コンシューマーが1つある場合、内部parallel領域も非アクティブであるため、タスクのオーバーヘッドなしでシリアルに実行されます。

2つのプロデューサーで実行する場合、外側のparallel領域はアクティブであるため、内側のparallel領域は非アクティブになり(ネストされた並列処理は有効になりません)、その結果、タスクはまったく作成されませんseek()。単純にシリアルに実行されます。タスクのオーバーヘッドはなく、コードは1プロデューサー/1コンシューマーの場合のほぼ2倍の速度で実行されます。スレッドの数が指定されていても、内部parallel領域は常に非アクティブであるため、実行時間はコンシューマーの数に依存しません。

共有変数へのタスクと協調アクセスがもたらすオーバーヘッドはどれくらいですか?次のコードを実行する単純な合成ベンチマークを作成しました。

for (int i = 0; i < 10000000; i++) {
   ssum += sin(i*0.001);
}

これは、デフォルトの最適化レベルがGCC4.7.2のWestmereCPUで1秒未満実行されます。atomic次に、共有変数へのアクセスを保護するための単純な構造を使用したタスクを紹介しましたssum

#pragma omp parallel
{
   #pragma omp single
   for (int i = 0; i < 10000000; i++) {
      #pragma omp task
      {
         #pragma omp atomic
         ssum += sin(i*0.001);
      }
   }
}

(リージョンtaskwaitの終わりにある暗黙のバリアにスケジューリングポイントがあるため、ここでは必要ありません)parallel

Massimilianoが提案したのと同じ方法でリダクションを実行するより複雑なバリアントも作成しました。

#define STRIDE 8

#pragma omp parallel
{
   #pragma omp single
   for (int i = 0; i < 10000000; i++) {
      #pragma omp task
      {
         const int idx = omp_get_thread_num();
         ssumt[idx*STRIDE] += sin(i*0.001);
      }
   }
   #pragma omp taskwait

   const int idx = omp_get_thread_num();
   #pragma omp atomic
   ssum += ssumt[idx*STRIDE];
}

コードは次のようにGCC4.7.2でコンパイルされました。

g++ -fopenmp -o test.exe test.cc

スレッド数とソケットへのスレッド配置が異なるデュアルソケットWestmereシステム(合計12コア)でバッチモードで実行すると(他のプロセスが介入できないように)、両方のコードで次の実行時間が得られます。

Configuration   ATOMIC       Reduction    ATOMIC slowdown
2 + 0            2,79 ±0,15   2,74 ±0,19   1,8%
1 + 1            2,72 ±0,21   2,51 ±0,22   8,4% <-----
6 + 0           10,14 ±0,01  10,12 ±0,01   0,2%
3 + 3           22,60 ±0,24  22,69 ±0,33  -0,4%
6 + 6           37,85 ±0,67  38,90 ±0,89  -2,7%

(実行時間は、によって測定された秒数で示されomp_get_wtime()、10回の実行の平均/標準偏差も示されています/。x + yConfigurationにはx、最初のソケットのyスレッドと2番目のソケットのスレッドを意味します)

ご覧のとおり、タスクのオーバーヘッドは膨大です。atomicこれは、スレッドプライベートアキュムレータに削減を適用する代わりに使用することによるオーバーヘッドよりもはるかに高くなります。さらに、atomicwithの割り当て部分は+=、ロックされたコンペアアンドスワップ命令()にコンパイルされます。これは、毎回LOCK CMPXCHG呼び出すよりもはるかに高いオーバーヘッドではありません。omp_get_thread_num()

また、デュアルソケットWestmereシステムはNUMAであることに注意してください。これは、各CPUに独自のメモリがあり、他のCPUのメモリへのアクセスがQPIリンクを経由するため、遅延が増加する(場合によっては帯域幅が狭くなる)ためです。この場合、ssum変数は共有されるためatomic、2番目のプロセッサで実行されるスレッドは基本的にリモート要求を行います。それでも、両方のコードの違いはごくわずかであり(マークされた2スレッドの場合を除いて、理由を調査する必要があります)、atomicスレッド数が増えると、コードは削減されたコードを上回り始めます。

マルチスケールNUMAシステムではatomic、すでに低速なリモートアクセスにロックのオーバーヘッドが追加されるため、アプローチでの同期はより大きな負担になる可能性があります。そのようなシステムの1つは、BCS結合ノードの1つです。BCS(Bull Coherence Switch)は、XQPI(eXternal QPI)を使用して複数のNehalem-EXボードを単一のシステムにリンクし、途中で3つのレベルのNUMA(ローカルメモリ、同じボード上のリモートメモリ)を導入するBull独自のソリューションです。 ;リモートボード上のリモートメモリ)。それぞれ4つのオクトコアNehalem-EXCPU(合計128コア)を備えた4つのボードで構成されるこのようなシステムの1つで実行すると、atomic実行可能ファイルは1036秒間実行され(!!)、削減アプローチは1047秒間実行されます。つまり、両方とも実行されます。ほぼ同じ時間(私の前の声明はatomicアプローチは21.5%遅くなりましたが、これは測定中のOSサービスのジッターが原因でした)。どちらの数値も1回の実行によるものであるため、完全に代表的なものではありません。このシステムでは、XQPIリンクによりボード間QPIメッセージの遅延が非常に大きくなるため、ロックは非常に高価ですが、それほど高価ではないことに注意してください。リダクションを使用することでオーバーヘッドの一部を取り除くことができますが、適切に実装する必要があります。まず、リダクション変数のローカルコピーは、スレッドが実行されるNUMAノードに対してもローカルである必要があります。次に、を呼び出さない方法を見つける必要がありますomp_get_thread_num()。これら2つはさまざまな方法で実現できますが、最も簡単な方法はthreadprivate変数を使用することです。

static double ssumt;
#pragma omp threadprivate(ssumt)

#pragma omp parallel
{
   ssumt = 0.0;

   #pragma omp single
   for (int i = 0; i < 10000000; i++) {
      #pragma omp task
      {
         ssumt += sin(i*0.001);
      }
   }
   #pragma omp taskwait

   #pragma omp atomic
   ssum += ssumt;
}

ssumt2つのタスクが同じスレッドで同時に実行されることはめったにないため、へのアクセスには保護は必要ありません(これがOpenMP仕様に準拠しているかどうかをさらに調査する必要があります)。このバージョンのコードは972秒間実行されます。繰り返しになりますが、これは1036秒からそれほど遠くはなく、単一の測定からのみ発生します(つまり、単に統計的な変動である可能性があります)が、理論的には、より高速であるはずです。

家に持ち帰るレッスン:

  • ネストparallel領域に関するOpenMP仕様をお読みください。ネストされた並列処理は、環境変数OMP_NESTEDをに設定するtrueか、を呼び出すことによって有効になりますomp_set_nested(1);OMP_MAX_ACTIVE_LEVELS有効にすると、Massimilianoが指摘したように、アクティブなネストのレベルを制御できます。
  • データの競合に注意し、最も単純なアプローチを使用してそれらを防ぐようにしてください。より複雑なアプローチを使用するたびに、パフォーマンスが向上するわけではありません。
  • 特別なシステムは通常、特別なプログラミングを必要とします。
  • 疑わしい場合は、可能であればスレッドチェッカーツールを使用してください。Intelには1つ(商用)があり、OracleのSolaris Studio(以前はSun Studioとして知られていました)にも1つあります(無料。製品名に関係なくLinuxバージョンがあります)。
  • オーバーヘッドに注意してください!何百万ものタスクを作成することによるオーバーヘッドが、取得した並列ゲインを無効にしないように、ジョブを十分な大きさのチャンクに分割してみてください。
于 2012-10-24T13:13:40.190 に答える
0

Hristo がコメントで既に提案したように、ネストされた並列処理を有効にする必要があります。これは、環境変数を設定して行われます。

  • OMP_NESTED(ネストされた並列処理を有効または無効にします)
  • OMP_MAX_ACTIVE_LEVELS(ネストされたアクティブな並列領域の最大数を制御します)

一方、atomic構築物で蓄積を保護するのではなく、次の戦略を提案します。

...
// Create a local buffer to accumulate partial results
const int nthreads = numCheckers/numSeekers;
const int stride   = 32; // Choose a value that avoids false sharing
int numt[stride*nthreads];
// Initialize to zero as we are reducing on + operator
for (int ii = 0; ii < stride*nthreads; ii++)    
  numt[ii] = 0;

#pragma omp parallel num_threads(numCheckers/numSeekers)
{

  //only one time for every producer
  #pragma omp single
  {
    for(int pos = 0; pos < n; pos += STEP){
      if (condition(buffer[pos])){
      #pragma omp task
      {
        //check() is a sequential function
        const int idx = omp_get_thread_num();
        numt[idx*stride] += check(buffer[pos]);
      }
    }
  }
  #pragma omp taskwait

  // Accumulate partial results
  const int idx = omp_get_thread_num();
  #pragma atomic
  num += numt[stride*idx];
}

これにより、同じメモリ位置への同時書き込み要求による潜在的な速度低下を防ぐことができます。

reduction 最も内側の並列領域での使用が次のように間違っていたことを示唆する以前のバージョンの回答に注意してください。

最も内側にあるワークシェアリングまたは並列構造の削減句に現れるリスト項目は、明示的なタスクでアクセスできない場合があります

OpenMP 3.1 仕様の §2.9.3.6 では許可されていません。

于 2012-10-23T20:03:05.277 に答える