私は C++ と CUDA/C を使用しており、特定の問題のコードを書きたいと思っています。非常にトリッキーなリダクションの問題に遭遇しました。
並列プログラミングでの私の経験は無視できるものではありませんが、非常に限られており、この問題の特異性を完全に予測することはできません。私が直面している問題を処理するための便利な、または「簡単な」方法があるとは思えませんが、おそらく私は間違っています。この問題または同様の問題をカバーするリソース (記事、書籍、Web リンクなど) またはキーワードがある場合は、お知らせください。
あまりにも多くのコードを投稿するのではなく、ケース全体を可能な限り一般化し、抽象化するようにしました。
レイアウト ...
N 個の初期要素と N 個の結果要素のシステムがあります。(たとえば、N=8 を使用しますが、N は 3 より大きい任意の整数値にすることができます。)
static size_t const N = 8;
double init_values[N], result[N];
自己干渉なしで、init-values のほぼすべての (すべてではありませんが) 一意の順列を計算する必要があります。
これは、計算f(init_values[0],init_values[1])
、f(init_values[0],init_values[2])
、 ...、f(init_values[0],init_values[N-1])
、f(init_values[1],init_values[2])
、 ... 、 ... などを意味f(init_values[1],init_values[N-1])
します。
これは、実際には、次の図に示す形状の仮想三角行列です。
P 0 1 2 3 4 5 6 7
|---------------------------------------
0| x
|
1| 0 x
|
2| 1 2 x
|
3| 3 4 5 x
|
4| 6 7 8 9 x
|
5| 10 11 12 13 14 x
|
6| 15 16 17 18 19 20 x
|
7| 21 22 23 24 25 26 27 x
各要素は、 のそれぞれの列要素と行要素の関数ですinit_values
。
P[i] (= P[row(i)][col(i]) = f(init_values[col(i)], init_values[row(i)])
すなわち
P[11] (= P[5][1]) = f(init_values[1], init_values[5])
例 を使用すると、可能な一意の組み合わせがあります(N*N-N)/2 = 28
(注: P[1][5]==P[5][1]
、したがって、下 (または上) 三角行列しかありません) N = 8
。
基本的な問題
結果の配列は、行要素の合計からそれぞれの列要素の合計を差し引いたものとして P から計算されます。たとえば、位置 3 の結果は、行 3 の合計から列 3 の合計を引いて計算されます。
result[3] = (P[3]+P[4]+P[5]) - (P[9]+P[13]+P[18]+P[24])
result[3] = sum_elements_row(3) - sum_elements_column(3)
N=4の絵で説明してみました。
結果として、次のことが当てはまります。
N-1
操作 (潜在的な同時書き込み) はそれぞれに対して実行されますresult[i]
result[i]
減算と加算N-(i+1)
からの書き込みがありますi
- それぞれからの発信
P[i][j]
には、への減算とへの加算がありr[j]
ますr[i]
ここで主な問題が発生します。
- 1 つのスレッドを使用して各 P を計算し、結果を直接更新すると、複数のカーネルが同じ結果の場所 (それぞれ N-1 スレッド) に書き込もうとすることになります。
- 一方、後続の削減ステップのために行列 P 全体を保存することは、メモリ消費の点で非常に高価であり、したがって、非常に大規模なシステムでは不可能です。
スレッドブロックごとに一意の共有結果ベクトルを持つという考えも不可能です。(N of 50k は 25 億の P 要素を作成するため、[ブロックあたりの最大数が 1024 スレッドであると仮定して] 各ブロックに 50k の倍精度要素を持つ独自の結果配列がある場合、最小数の 240 万ブロックが 900GiB を超えるメモリを消費します。)
より静的な動作の削減を処理できると思いますが、この問題は潜在的な同時メモリ書き込みアクセスに関してかなり動的です。(または、「基本的な」タイプの削減で処理できますか?)
いくつかの複雑さを追加します...
残念ながら、初期値に依存しない (任意のユーザー) 入力によっては、P の一部の要素をスキップする必要があります。順列 P[6]、P[14]、および P[18] をスキップする必要があるとします。したがって、計算する必要がある 24 の組み合わせが残っています。
どの値をスキップする必要があるかをカーネルに伝える方法は? N が非常に大きい場合 (数万の要素など)、それぞれに顕著な欠点があります。
1.すべての組み合わせを保存...
...それぞれの行と列の indexを使用して、 a で計算し、このベクトルで操作するstruct combo { size_t row,col; };
必要があります。vector<combo>
(現在の実装で使用)
std::vector<combo> elements;
// somehow fill
size_t const M = elements.size();
for (size_t i=0; i<M; ++i)
{
// do the necessary computations using elements[i].row and elements[i].col
}
このソリューションは、「数」(数万の要素でさえあるかもしれませんが、合計で数十億とは対照的ではありません)であるため、大量のメモリを消費しますが、回避します
- 指数計算
- 削除された要素の検索
これは、2 番目のアプローチの欠点です。
2. P のすべての要素を操作し、削除された要素を見つける
P の各要素を操作し、ネストされたループ (cuda ではうまく再現できませんでした) を回避したい場合は、次のようにする必要があります。
size_t M = (N*N-N)/2;
for (size_t i=0; i<M; ++i)
{
// calculate row indices from `i`
double tmp = sqrt(8.0*double(i+1))/2.0 + 0.5;
double row_d = floor(tmp);
size_t current_row = size_t(row_d);
size_t current_col = size_t(floor(row_d*(ict-row_d)-0.5));
// check whether the current combo of row and col is not to be removed
if (!removes[current_row].exists(current_col))
{
// do the necessary computations using current_row and current_col
}
}
このベクトルremoves
は、最初の例のベクトルとは対照的に非常に小さいですが、elements
を取得するための追加の計算と if 分岐は非常に非効率的です。(まだ数十億の評価について話していることを思い出してください。)current_row
current_col
3. P のすべての要素を操作し、後で要素を削除する
私が持っていた別のアイデアは、すべての有効な組み合わせと無効な組み合わせを個別に計算することでした。残念ながら、合計エラーのため、次のステートメントは true です。
calc_non_skipped() != calc_all() - calc_skipped()
初期値から目的の結果を得る便利で既知の高性能な方法はありますか?
この質問はかなり複雑で、おそらく関連性が限られていることを私は知っています。それにもかかわらず、私の問題を解決するのにいくつかの明快な答えが役立つことを願っています.
現在の実装
現在、これは OpenMP を使用した CPU コードとして実装されています。combo
最初に、計算が必要なすべての P を格納する上記の s のベクトルを設定し、それを並列 for ループに渡します。各スレッドにはプライベート結果ベクトルが提供され、並列領域の最後にあるクリティカル セクションが適切な合計に使用されます。