GPU甚の単玔なハッシュテヌブル

GPU甚の単玔なハッシュテヌブル
Githubに投皿したした 新しいプロゞェクト シンプルな GPU ハッシュ テヌブル.

これは、1060 秒あたり数億件の挿入を凊理できるシンプルな GPU ハッシュ テヌブルです。 私の NVIDIA GTX 64 ラップトップでは、コヌドはランダムに生成された 210 䞇のキヌず倀のペアを玄 32 ミリ秒で挿入し、64 䞇のペアを玄 XNUMX ミリ秒で削陀したす。

぀たり、ラップトップの速床は玄 300 億挿入/秒、500 億削陀/秒です。

テヌブルは CUDA で䜜成されおいたすが、同じ手法を HLSL たたは GLSL に適甚するこずもできたす。 ビデオ カヌドで高いパフォヌマンスを確保するには、実装にはいく぀かの制限がありたす。

  • 32 ビットのキヌず同じ倀のみが凊理されたす。
  • ハッシュ テヌブルのサむズは固定です。
  • そしお、このサむズは XNUMX のべき乗に等しくなければなりたせん。

キヌず倀に぀いおは、単玔な区切りマヌカヌを予玄する必芁がありたす (䞊蚘のコヌドでは、これは 0xffffffff です)。

ロックのないハッシュ テヌブル

ハッシュ テヌブルはオヌプン アドレス指定を䜿甚したす。 リニアプロヌビング぀たり、メモリに保存され、優れたキャッシュ パフォヌマンスを備えた単なるキヌず倀のペアの配列です。 同じこずは、リンクされたリスト内のポむンタヌの怜玢を䌎う連鎖の堎合には蚀えたせん。 ハッシュ テヌブルは芁玠を栌玍する単玔な配列です KeyValue:

struct KeyValue
{
    uint32_t key;
    uint32_t value;
};

pow2/AND マスクを適甚するには XNUMX ぀の高速呜什で十分ですが、モゞュラス挔算子ははるかに遅いため、テヌブルのサむズは玠数ではなく XNUMX のべき乗です。 線圢テヌブル ルックアップではスロット むンデックスを各スロットでラップする必芁があるため、これは線圢プロヌブの堎合に重芁です。 その結果、操䜜のコストがスロットごずにモゞュロで远加されたす。

テヌブルには各芁玠のキヌず倀のみが保存され、キヌのハッシュは保存されたせん。 テヌブルには 32 ビットのキヌのみが栌玍されるため、ハッシュは非垞に迅速に蚈算されたす。 䞊蚘のコヌドは Murmur3 ハッシュを䜿甚しおおり、数回のシフト、XOR、乗算のみを実行したす。

ハッシュ テヌブルは、メモリの順序に䟝存しないロック保護技術を䜿甚したす。 䞀郚の曞き蟌み操䜜によっお他のそのような操䜜の順序が混乱した堎合でも、ハッシュ テヌブルは䟝然ずしお正しい状態を維持したす。 これに぀いおは以䞋で説明したす。 この技術は、数千のスレッドを同時に実行するビデオ カヌドでうたく機胜したす。

ハッシュ テヌブルのキヌず倀は空に初期化されたす。

コヌドを倉曎しお、64 ビットのキヌず倀も凊理するこずもできたす。 キヌにはアトミックな読み取り、曞き蟌み、比范および亀換の操䜜が必芁です。 たた、倀にはアトミックな読み取りおよび曞き蟌み操䜜が必芁です。 幞いなこずに、CUDA では、32 ビット倀ず 64 ビット倀の読み取り/曞き蟌み操䜜は、それらが自然にアラむメントされおいる限りアトミックです (以䞋を参照)。 ここで)、最新のビデオ カヌドは 64 ビットのアトミック比范および亀換操䜜をサポヌトしおいたす。 もちろん、64 ビットに移行するず、パフォヌマンスは若干䜎䞋したす。

ハッシュテヌブルの状態

ハッシュ テヌブル内の各キヌず倀のペアは、次の XNUMX ぀の状態のいずれかになりたす。

  • キヌず倀が空です。 この状態でハッシュテヌブルが初期化されたす。
  • キヌは曞き留められおいたすが、倀はただ曞き蟌たれおいたせん。 別のスレッドが珟圚デヌタを読み取っおいる堎合、そのスレッドは空を返したす。 これは正垞なこずであり、別の実行スレッドがもう少し早く動䜜しおいれば同じこずが起こっおいたでしょう。ここで話しおいるのは同時デヌタ構造です。
  • キヌず倀の䞡方が蚘録されたす。
  • 倀は他の実行スレッドで䜿甚できたすが、キヌはただ䜿甚できたせん。 これは、CUDA プログラミング モデルのメモリ モデルの順序が緩いために発生する可胜性がありたす。 これは正垞であり、いずれにしおも、倀が空でなくなっおも、キヌは空のたたです。

重芁なニュアンスは、キヌがスロットに曞き蟌たれるず、キヌが削陀されおも移動しなくなるずいうこずです。これに぀いおは以䞋で説明したす。

ハッシュ テヌブル コヌドは、メモリが読み曞きされる順序が䞍明な、緩やかに順序付けされたメモリ モデルでも機胜したす。 ハッシュ テヌブルでの挿入、怜玢、削陀を確認するずきは、各キヌず倀のペアが䞊蚘の XNUMX ぀の状態のいずれかにあるこずに泚意しおください。

ハッシュテヌブルぞの挿入

キヌず倀のペアをハッシュ テヌブルに挿入する CUDA 関数は次のようになりたす。

void gpu_hashtable_insert(KeyValue* hashtable, uint32_t key, uint32_t value)
{
    uint32_t slot = hash(key);

    while (true)
    {
        uint32_t prev = atomicCAS(&hashtable[slot].key, kEmpty, key);
        if (prev == kEmpty || prev == key)
        {
            hashtable[slot].value = value;
            break;
        }
        slot = (slot + 1) & (kHashTableCapacity-1);
    }
}

キヌを挿入するには、コヌドは挿入されたキヌのハッシュから始たるハッシュ テヌブル配列を反埩凊理したす。 配列内の各スロットは、そのスロット内のキヌを空ず比范するアトミックな比范亀換操䜜を実行したす。 䞍䞀臎が怜出された堎合、スロット内のキヌは挿入されたキヌで曎新され、元のスロット キヌが返されたす。 この元のキヌが空であるか、挿入されたキヌず䞀臎する堎合、コヌドは挿入に適したスロットを芋぀けお、挿入された倀をそのスロットに挿入したす。

XNUMX ぀のカヌネル呌び出しの堎合 gpu_hashtable_insert() 同じキヌを持぀芁玠が耇数ある堎合、それらの倀のいずれかをキヌ スロットに曞き蟌むこずができたす。 これは正垞であるず考えられたす。呌び出し䞭のキヌず倀の曞き蟌みの XNUMX ぀は成功したすが、これらすべおが耇数の実行スレッド内で䞊行しお行われるため、どのメモリぞの曞き蟌みが最埌の曞き蟌みになるかを予枬できたせん。

ハッシュテヌブルルックアップ

キヌを怜玢するコヌド:

uint32_t gpu_hashtable_lookup(KeyValue* hashtable, uint32_t key)
{
        uint32_t slot = hash(key);

        while (true)
        {
            if (hashtable[slot].key == key)
            {
                return hashtable[slot].value;
            }
            if (hashtable[slot].key == kEmpty)
            {
                return kEmpty;
            }
            slot = (slot + 1) & (kHashTableCapacity - 1);
        }
}

テヌブルに栌玍されおいるキヌの倀を芋぀けるには、探しおいるキヌのハッシュから始たる配列を反埩凊理したす。 各スロットで、キヌが探しおいるものであるかどうかを確認し、そうである堎合はその倀を返したす。 たた、キヌが空かどうかも確認し、空の堎合は怜玢を䞭止したす。

キヌが芋぀からない堎合、コヌドは空の倀を返したす。

これらの怜玢操䜜はすべお、挿入ず削陀を通じお同時に実行できたす。 テヌブル内の各ペアは、フロヌに関しお䞊で説明した XNUMX ぀の状態のうちの XNUMX ぀を持ちたす。

ハッシュテヌブルでの削陀

キヌを削陀するコヌド:

void gpu_hashtable_delete(KeyValue* hashtable, uint32_t key, uint32_t value)
{
    uint32_t slot = hash(key);

    while (true)
    {
        if (hashtable[slot].key == key)
        {
            hashtable[slot].value = kEmpty;
            return;
        }
        if (hashtable[slot].key == kEmpty)
        {
            return;
        }
        slot = (slot + 1) & (kHashTableCapacity - 1);
    }
}

キヌの削陀は通垞ずは異なる方法で行われたす。キヌをテヌブルに残し、その倀 (キヌ自䜓ではなく) を空ずしおマヌクしたす。 このコヌドは以䞋に非垞によく䌌おいたす lookup()ただし、キヌで䞀臎が芋぀かった堎合、その倀は空になりたす。

前述したように、キヌがスロットに曞き蟌たれるず、それ以降は移動されなくなりたす。 芁玠がテヌブルから削陀されおも、キヌはそのたた残り、その倀は単に空になりたす。 これは、珟圚の倀が空かどうかは関係なく、䟝然ずしお空になるため、スロット倀に察しおアトミックな曞き蟌み操䜜を䜿甚する必芁がないこずを意味したす。

ハッシュテヌブルのサむズ倉曎

ハッシュ テヌブルのサむズを倉曎するには、より倧きなテヌブルを䜜成し、叀いテヌブルから空でない芁玠をそこに挿入したす。 サンプル コヌドをシンプルに保ちたかったので、この機胜は実装したせんでした。 さらに、CUDA プログラムでは、メモリ割り圓おは CUDA カヌネルではなくホスト コヌドで行われるこずがよくありたす。

蚘事 ロックフリヌ、りェむトフリヌのハッシュテヌブル では、このようなロックで保護されたデヌタ構造を倉曎する方法に぀いお説明したす。

競争力

䞊蚘の関数コヌドスニペットでは gpu_hashtable_insert(), _lookup() О _delete() 䞀床に XNUMX ぀のキヌず倀のペアを凊理したす。 そしお、より䜎い gpu_hashtable_insert(), _lookup() О _delete() ペアの配列を䞊列に凊理し、各ペアを個別の GPU 実行スレッドで凊理したす。

// CPU code to invoke the CUDA kernel on the GPU
uint32_t threadblocksize = 1024;
uint32_t gridsize = (numkvs + threadblocksize - 1) / threadblocksize;
gpu_hashtable_insert_kernel<<<gridsize, threadblocksize>>>(hashtable, kvs, numkvs);

// GPU code to process numkvs key/values in parallel
void gpu_hashtable_insert_kernel(KeyValue* hashtable, const KeyValue* kvs, unsigned int numkvs)
{
    unsigned int threadid = blockIdx.x*blockDim.x + threadIdx.x;
    if (threadid < numkvs)
    {
        gpu_hashtable_insert(hashtable, kvs[threadid].key, kvs[threadid].value);
    }
}

ロック耐性のあるハッシュ テヌブルは、同時挿入、怜玢、および削陀をサポヌトしたす。 キヌず倀のペアは垞に XNUMX ぀の状態のいずれかにあり、キヌは移動しないため、異なる皮類の操䜜が同時に䜿甚された堎合でも、テヌブルの正確性が保蚌されたす。

ただし、挿入ず削陀のバッチを䞊行しお凊理し、ペアの入力配列に重耇キヌが含たれおいる堎合、どのペアが「勝぀」か、぀たりハッシュ テヌブルに最埌に曞き蟌たれるペアを予枬するこずはできたせん。 ペアの入力配列を䜿甚しお挿入コヌドを呌び出したずしたす。 A/0 B/1 A/2 C/3 A/4。 コヌドが完了したら、ペアリングしたす B/1 О C/3 テヌブル内に存圚するこずが保蚌されおいたすが、同時にいずれかのペアがテヌブル内に衚瀺されたす。 A/0, A/2 たたは A/4。 これは問題になる堎合もあれば、問題にならない堎合もありたす。すべおはアプリケヌションによっお異なりたす。 入力配列に重耇キヌがないこずが事前にわかっおいる堎合や、どの倀が最埌に曞き蟌たれたかを気にしない堎合もありたす。

これが問題になる堎合は、重耇したペアを異なる CUDA システム コヌルに分割する必芁がありたす。 CUDA では、カヌネルを呌び出す操䜜はすべお、次のカヌネル呌び出しの前に垞に完了したす (少なくずも XNUMX ぀のスレッド内では。異なるスレッドでは、カヌネルは䞊列実行されたす)。 䞊蚘の䟋で、XNUMX ぀のカヌネルを次のように呌び出した堎合、 A/0 B/1 A/2 C/3、および他の A/4、次にキヌ A 倀を取埗したす 4.

次に、関数が次のようにすべきかどうかに぀いお話したしょう。 lookup() О delete() ハッシュ テヌブル内のペアの配列ぞのプレヌンたたは揮発性ポむンタを䜿甚したす。 CUDA ドキュメント 次のように述べおいたす。

コンパむラは、グロヌバル メモリたたは共有メモリぞの読み取りず曞き蟌みを最適化するこずを遞択できたす。これらの最適化は、キヌワヌドを䜿甚しお無効にできたす。 volatile: ... この倉数ぞの参照はすべお、実メモリの読み取りたたは曞き蟌み呜什にコンパむルされたす。

正確性に関する考慮事項には適甚は必芁ありたせん volatile。 実行スレッドが以前の読み取り操䜜でキャッシュされた倀を䜿甚する堎合、わずかに叀い情報を䜿甚するこずになりたす。 ただし、これはカヌネル呌び出しのある時点でのハッシュ テヌブルの正しい状態からの情報です。 最新の情報を䜿甚する必芁がある堎合は、むンデックスを䜿甚できたす。 volatileただし、その埌パフォヌマンスはわずかに䜎䞋したす。私のテストによるず、32 䞇個の芁玠を削陀するず、速床は 500 億削陀/秒から 450 億 XNUMX 䞇削陀/秒に䜎䞋したした。

ПрПОзвПЎОтельМПсть

64 䞇個の芁玠を挿入し、そのうち 32 䞇個を削陀するテストでは、 std::unordered_map そしお、GPU のハッシュ テヌブルは事実䞊ありたせん。

GPU甚の単玔なハッシュテヌブル
std::unordered_map 芁玠の挿入ず削陀、および芁玠の解攟に 70 ミリ秒を費やしたした unordered_map (䜕癟䞇もの芁玠を削陀するには倚倧な時間がかかりたす。 unordered_map 耇数のメモリ割り圓おが行われたす)。 正盎蚀えば、 std:unordered_map 党く異なる制限。 これは単䞀の CPU スレッドで実行され、あらゆるサむズの Key-Value をサポヌトし、高い䜿甚率で優れたパフォヌマンスを発揮し、耇数の削陀埌も安定したパフォヌマンスを瀺したす。

GPU ずプログラム間通信のハッシュ テヌブルの継続時間は 984 ミリ秒でした。 これには、テヌブルのメモリぞの配眮ず削陀 (䞀床に 1 GB のメモリを割り圓おるため、CUDA では時間がかかりたす)、芁玠の挿入ず削陀、およびそれらの反埩に費やされる時間が含たれたす。 ビデオ カヌド メモリずの間のすべおのコピヌも考慮されたす。

ハッシュ テヌブル自䜓が完了するたでに 271 ミリ秒かかりたした。 これには、ビデオ カヌドが芁玠の挿入ず削陀に費やした時間が含たれたすが、メモリぞのコピヌず結果のテヌブルの反埩に費やした時間は考慮されおいたせん。 GPU テヌブルが長期間存続する堎合、たたはハッシュ テヌブル党䜓がビデオ カヌドのメモリ内に含たれおいる堎合 (たずえば、䞭倮プロセッサではなく他の GPU コヌドで䜿甚されるハッシュ テヌブルを䜜成する堎合)、テスト結果が関係したす。

ビデオ カヌドのハッシュ テヌブルは、高スルヌプットずアクティブな䞊列化により高いパフォヌマンスを発揮したす。

制限事項

ハッシュ テヌブルのアヌキテクチャには、泚意すべき問題がいく぀かありたす。

  • 線圢プロヌブはクラスタリングによっお劚げられ、テヌブル内のキヌが完党に配眮されなくなりたす。
  • 関数を䜿甚しおキヌは削陀されたせん delete そしお時間が経぀ずテヌブルが散らかっおしたいたす。

その結果、特にハッシュ テヌブルが長期間存圚し、倚数の挿入ず削陀が行われた堎合、ハッシュ テヌブルのパフォヌマンスが埐々に䜎䞋する可胜性がありたす。 これらの欠点を軜枛する XNUMX ぀の方法は、かなり䜎い䜿甚率で新しいテヌブルに再ハッシュし、再ハッシュ䞭に削陀されたキヌをフィルタヌで陀倖するこずです。

説明した問題を説明するために、䞊蚘のコヌドを䜿甚しお 128 億 4 䞇の芁玠を持぀テヌブルを䜜成し、124 億 0,96 䞇のスロットが埋たるたで 4 䞇の芁玠をルヌプしたす (䜿甚率は玄 XNUMX)。 これは結果テヌブルです。各行は、XNUMX 䞇の新しい芁玠を XNUMX ぀のハッシュ テヌブルに挿入するための CUDA カヌネル呌び出しです。

䜿甚率
挿入時間 4 芁玠

0,00
11,608448 ミリ秒 (361,314798 䞇キヌ/秒)

0,03
11,751424 ミリ秒 (356,918799 䞇キヌ/秒)

0,06
11,942592 ミリ秒 (351,205515 䞇キヌ/秒)

0,09
12,081120 ミリ秒 (347,178429 䞇キヌ/秒)

0,12
12,242560 ミリ秒 (342,600233 䞇キヌ/秒)

0,16
12,396448 ミリ秒 (338,347235 䞇キヌ/秒)

0,19
12,533024 ミリ秒 (334,660176 䞇キヌ/秒)

0,22
12,703328 ミリ秒 (330,173626 䞇キヌ/秒)

0,25
12,884512 ミリ秒 (325,530693 䞇キヌ/秒)

0,28
13,033472 ミリ秒 (321,810182 䞇キヌ/秒)

0,31
13,239296 ミリ秒 (316,807174 䞇キヌ/秒)

0,34
13,392448 ミリ秒 (313,184256 䞇キヌ/秒)

0,37
13,624000 ミリ秒 (307,861434 䞇キヌ/秒)

0,41
13,875520 ミリ秒 (302,280855 䞇キヌ/秒)

0,44
14,126528 ミリ秒 (296,909756 䞇キヌ/秒)

0,47
14,399328 ミリ秒 (291,284699 䞇キヌ/秒)

0,50
14,690304 ミリ秒 (285,515123 䞇キヌ/秒)

0,53
15,039136 ミリ秒 (278,892623 䞇キヌ/秒)

0,56
15,478656 ミリ秒 (270,973402 䞇キヌ/秒)

0,59
15,985664 ミリ秒 (262,379092 䞇キヌ/秒)

0,62
16,668673 ミリ秒 (251,627968 䞇キヌ/秒)

0,66
17,587200 ミリ秒 (238,486174 䞇キヌ/秒)

0,69
18,690048 ミリ秒 (224,413765 䞇キヌ/秒)

0,72
20,278816 ミリ秒 (206,831789 䞇キヌ/秒)

0,75
22,545408 ミリ秒 (186,038058 䞇キヌ/秒)

0,78
26,053312 ミリ秒 (160,989275 䞇キヌ/秒)

0,81
31,895008 ミリ秒 (131,503463 䞇キヌ/秒)

0,84
42,103294 ミリ秒 (99,619378 䞇キヌ/秒)

0,87
61,849056 ミリ秒 (67,815164 䞇キヌ/秒)

0,90
105,695999 ミリ秒 (39,682713 䞇キヌ/秒)

0,94
240,204636 ミリ秒 (17,461378 䞇キヌ/秒)

䜿甚率が増加するず、パフォヌマンスが䜎䞋したす。 これはほずんどの堎合、望たしくないこずです。 アプリケヌションがテヌブルに芁玠を挿入し、その埌それらを砎棄する堎合 (たずえば、本の䞭の単語を数える堎合)、これは問題ではありたせん。 ただし、アプリケヌションが長期間存続するハッシュ テヌブルを䜿甚する堎合 (たずえば、ナヌザヌが情報を頻繁に挿入および削陀する画像の空でない郚分を保存するグラフィック ゚ディタヌなど)、この動䜜は問題ずなる可胜性がありたす。

そしお、64 䞇回の挿入埌のハッシュ テヌブルのプロヌブの深さを枬定したした (利甚率 0,5)。 平均の深さは 0,4774 であったため、ほずんどのキヌは可胜な限り最適なスロット、たたは最適な䜍眮から 60 スロット離れた堎所にありたした。 最倧深床はXNUMXでした。

次に、124 億 0,97 䞇個のむンサヌトを備えたテヌブルのプロヌブ深さを枬定したした (利甚率 10,1757)。 平均深床はすでに XNUMX であり、最倧深床は - 6474 (!!)。 䜿甚率が高くなるず、リニア センシングのパフォヌマンスが倧幅に䜎䞋したす。

このハッシュ テヌブルの䜿甚率を䜎く保぀こずが最善です。 ただし、メモリ消費を犠牲にしおパフォヌマンスを向䞊させたす。 幞いなこずに、32 ビットのキヌず倀の堎合、これは正圓化できたす。 䞊蚘の䟋で、128 億 0,25 䞇個の芁玠を含むテヌブルで䜿甚率を 32 に維持するず、そこに配眮できる芁玠は 96 䞇個たでずなり、残りの 8 䞇個のスロット (ペアごずに 768 バむト) が倱われたす。 , 倱われた蚘憶はXNUMXMB。

システム メモリよりも貎重なリ゜ヌスであるビデオ カヌド メモリの損倱に぀いお話しおいるこずに泚意しおください。 CUDA をサポヌトするほずんどの最新のデスクトップ グラフィックス カヌドには少なくずも 4 GB のメモリが搭茉されおいたすが (この蚘事の執筆時点では、NVIDIA 2080 Ti には 11 GB のメモリが搭茉されおいたす)、それでもそのような量を倱うのは賢明な決定ずは蚀えたせん。

埌で、プロヌブの深さに問題がないビデオ カヌドのハッシュ テヌブルの䜜成ず、削陀されたスロットを再利甚する方法に぀いお詳しく曞きたす。

枬深深床枬定

キヌのプロヌブの深さを決定するには、実際のテヌブル むンデックスからキヌのハッシュ (理想的なテヌブル むンデックス) を抜出したす。

// get_key_index() -> index of key in hash table
uint32_t probelength = (get_key_index(key) - hash(key)) & (hashtablecapacity-1);

1 の 3 の補数の 4 進数の魔法ず、ハッシュ テヌブルの容量が XNUMX の XNUMX 乗であるずいう事実により、このアプロヌチは、キヌ むンデックスがテヌブルの先頭に移動された堎合でも機胜したす。 XNUMX にハッシュされ、スロット XNUMX に挿入されるキヌを考えおみたしょう。次に、容量 XNUMX のテヌブルの堎合、次の結果が埗られたす。 (3 — 1) & 3、これは 2 に盞圓したす。

たずめ

ご質問やご意芋がございたしたら、次のアドレスにメヌルしおください。 Twitter たたはで新しいトピックを開きたす リポゞトリ.

このコヌドは、優れた蚘事からむンスピレヌションを受けお䜜成されたした。

今埌も、ビデオ カヌドのハッシュ テヌブルの実装に぀いお曞き、そのパフォヌマンスを分析しおいきたす。 私の蚈画には、GPU に適したデヌタ構造でのアトミック操䜜を䜿甚したチェヌン化、ロビン フッド ハッシュ、およびカッコヌ ハッシュが含たれたす。

出所 habr.com

コメントを远加したす