これは、並列化しようとしている疑似コード (word2vec C コードから取得) です。まず、データ構造とそれに対応するサイズをリストし、次に疑似コードをリストします。
1. long long sen[MAX_SENTENCE_LENGTH]
// In the C code, MAX_SENTENCE_LENGTH = 1000. Increasing this should be
//fine.
2. float neu1[N] (hidden layer values)
//N is the length of each vector. For now, max N = 400
3. float neu1e[N] (hidden layer error values)
4. float syn0[V * N] (input to hidden layer weight matrix)
// For now, we can assume that V * N is small enough to be stored on the GPU
// In the test data, V = 72k words
5. float syn1neg[V * N] (back propagation weights used during negative
sampling)
6. float exptable[1000]
プログラムへの入力はテキスト ファイルです。次に、プログラムはそれを一度に 1 語ずつ処理して語彙を構築します。たとえば、テキスト ファイルに次の文があるとします。
「並列プログラミングは非常に興味深い」</p>
すると、語彙は次のようになります (コードは単語の頻度に基づいて語彙を並べ替えるため)。
{“Very:2”, “Parallel:1”, “programming:1”, “is:1”, “interesting:1”}
0 1 2 3 4
語彙を構築した後、コードは一度に 1000 語ずつテキストの処理を再開します。最初の 1000 語が に保存されsen[MAX_SENTENCE_LENGTH]
、次にニューラル ネットワークが 内のすべての語についてトレーニングされsen
、このプロセスがファイルの最後に到達するまで続きます。上記の文の場合、次のようにsen
なります[1,2,3,0,0,4]
。
トレーニングが 1 回の反復でのみ行われると仮定すると、擬似コードは次のようになります。
for sen in text
{
for word in sen
{
for (c = 0; c < N; c++)
neu1[c] = 0;
for (c = 0; c < N; c++)
neu1e[c] = 0;
/*The variable window is a user supplied parameter.
It is used to consider the context around a word in a sentence.
For example, if I am looking at the first word in the sentence
(target word is word1), and window = 5, then the words in the
window = {word2, word3, word4, word5}.
If I am looking at the third word in the sentence
(target word is word3), then window = {word1, word2, word4, word5}*/
for word in window
{
for (c = 0; c < N; c++)
neu1[c] += syn0[c + word * N];
}
for (c = 0; c < N; c++)
neu1[c] /= window;
//negative: number of negative samples to provide (assume it to be
//between 5 to 25)
for (d = 0; d < negative + 1; d++)
{
target = sen[random_index]
l2 = target * N;
f = 0;
for (c = 0; c < N; c++)
f += neu1[c] * syn1neg[c + l2];
gradient = exptable[function of f] //f is calculated in the loop above
for (c = 0; c < N; c++)
neu1e[c] += gradient * syn1neg[c + l2];
for (c = 0; c < N; c++)
syn1neg[c + l2] += gradient * neu1[c];
} //Negative Sampling ends
for word in window
{
for (c = 0; c < N; c++)
syn0[c + word * N] += neu1e[c];
}
} // word in sen loop ends
} // sen in text loop ends
これを並列化する最善の方法は、文中の単語を並列処理することだと考えています。N
すべてのループを考慮すると、単一のスレッドがループごとに 1 回だけグローバル メモリ ( ) にアクセスするように、ワードごとにスレッドを使用する必要があると思いsyn0, syn1neg
ます。また、neu1
とのneu1e
更新はすべて独立しているため、スレッドのプライベート メモリに常駐し、個別に更新することができます。
現在の主な懸念事項は次のとおりです。
- グローバル メモリ アクセスは、変数の値 (ボキャブラリのインデックス) に基づいてアクセスされるため
syn0
、ランダムな方法で発生しています。そして、ご覧のとおり、文中の単語は順不同です。syn1neg
word
これは大きな問題ですか?または、GPU に十分な数のスレッドを与えることで、メモリのレイテンシを隠すことはできますか? また、N スレッド/ワードが syn0 および syn1neg のシーケンシャル データにアクセスするため、このアクセス パターンが実際にランダムかどうかはわかりませんが、N スレッドの次のセットはメモリ内の遠くにあるシーケンシャル データにアクセスする可能性があります。
- ネガティブ サンプリング ループでは、リダクション操作を実行する必要があります。変数
f
は内積の合計です。問題は、グローバルメモリにあるのneu1
に対し、各スレッドのプライベートメモリに格納することを計画していることです。syn1neg
ネガティブ サンプリングには別のカーネルが必要ですか? Nスレッド/ワードを起動するだけではなく、別のアプローチが必要なようですが、どのアプローチが最適かはわかりません。
これらの懸念とは別に、このコードへのアプローチ方法に問題があるかどうかを提案してください。