web-dev-qa-db-ja.com

このコードをGPUで実行できますか/すべきですか?

私は、配列に約1,000万から3,000万の浮動小数点値を含む統計アプリケーションに取り組んでいます。

ネストされたループ内の配列に対して、異なるが独立した計算を実行するいくつかのメソッド、たとえば:

Dictionary<float, int> noOfNumbers = new Dictionary<float, int>();

for (float x = 0f; x < 100f; x += 0.0001f) {
    int noOfOccurrences = 0;

    foreach (float y in largeFloatingPointArray) {
        if (x == y) {
            noOfOccurrences++;
        }
    }

    noOfNumbers.Add(x, noOfOccurrences);
}

現在のアプリケーションはC#で記述されており、Intel CPUで実行され、完了するまでに数時間かかります。 GPUプログラミングの概念とAPIの知識がないため、私の質問は次のとおりです。

  • GPUを使用してこのような計算を高速化することは可能ですか(また、意味がありますか)?
  • 「はい」の場合:誰でもチュートリアルを知っているか、サンプルコードを入手していますか(プログラミング言語は関係ありません)?

どんな助けも大歓迎です。

42
Mike

[〜#〜] update [〜#〜]GPUバージョン

__global__ void hash (float *largeFloatingPointArray,int largeFloatingPointArraySize, int *dictionary, int size, int num_blocks)
{
    int x = (threadIdx.x + blockIdx.x * blockDim.x); // Each thread of each block will
    float y;                                         // compute one (or more) floats
    int noOfOccurrences = 0;
    int a;

    while( x < size )            // While there is work to do each thread will:
    {
        dictionary[x] = 0;       // Initialize the position in each it will work
        noOfOccurrences = 0;    

        for(int j = 0 ;j < largeFloatingPointArraySize; j ++) // Search for floats
        {                                                     // that are equal 
                                                             // to it assign float
           y = largeFloatingPointArray[j];  // Take a candidate from the floats array 
           y *= 10000;                      // e.g if y = 0.0001f;
           a = y + 0.5;                     // a = 1 + 0.5 = 1;
           if (a == x) noOfOccurrences++;    
        }                                      

        dictionary[x] += noOfOccurrences; // Update in the dictionary 
                                          // the number of times that the float appears 

    x += blockDim.x * gridDim.x;  // Update the position here the thread will work
    }
}

私はラップトップをテストしているので、これは小さな入力でテストしました。それでも、うまくいきました。ただし、精巣を進める必要があります。

[〜#〜] update [〜#〜]順次バージョン

アルゴリズムを20秒未満で30,000,000実行するこの単純なバージョンを実行しました(データを生成するための既にカウント機能)。

基本的に、フロートの配列をソートします。ソートされた配列を移動し、値が配列に連続して出現する回数を分析し、出現した回数とともにこの値を辞書に入れます。

私が使用したunordered_mapの代わりに、ソートされたマップを使用できます。

コードは次のとおりです。

#include <stdio.h>
#include <stdlib.h>
#include "cuda.h"
#include <algorithm>
#include <string>
#include <iostream>
#include <tr1/unordered_map>


typedef std::tr1::unordered_map<float, int> Mymap;


void generator(float *data, long int size)
{
    float LO = 0.0;
    float HI = 100.0;

    for(long int i = 0; i < size; i++)
        data[i] = LO + (float)Rand()/((float)Rand_MAX/(HI-LO));
}

void print_array(float *data, long int size)
{

    for(long int i = 2; i < size; i++)
        printf("%f\n",data[i]);

}

std::tr1::unordered_map<float, int> fill_dict(float *data, int size)
{
    float previous = data[0];
    int count = 1;
    std::tr1::unordered_map<float, int> dict;

    for(long int i = 1; i < size; i++)
    {
        if(previous == data[i])
            count++;
        else
        {
          dict.insert(Mymap::value_type(previous,count));
          previous = data[i];
          count = 1;         
        }

    }
    dict.insert(Mymap::value_type(previous,count)); // add the last member
    return dict;

}

void printMAP(std::tr1::unordered_map<float, int> dict)
{
   for(std::tr1::unordered_map<float, int>::iterator i = dict.begin(); i != dict.end(); i++)
  {
     std::cout << "key(string): " << i->first << ", value(int): " << i->second << std::endl;
   }
}


int main(int argc, char** argv)
{
  int size = 1000000; 
  if(argc > 1) size = atoi(argv[1]);
  printf("Size = %d",size);

  float data[size];
  using namespace __gnu_cxx;

  std::tr1::unordered_map<float, int> dict;

  generator(data,size);

  sort(data, data + size);
  dict = fill_dict(data,size);

  return 0;
}

ライブラリスラストがマシンにインストールされている場合、これを使用する必要があります。

#include <thrust/sort.h>
thrust::sort(data, data + size);

これの代わりに

sort(data, data + size);

確かに高速になります。

オリジナルポスト

「私は1000万から3000万の浮動小数点値を含む大きな配列を持つ統計アプリケーションに取り組んでいます」。

「GPUを使用してこのような計算を高速化することは可能ですか(そしてそれは理にかなっていますか?)」

はい、そうです。 1か月前、私は分子動力学シミュレーションを完全にGPUで行いました。粒子のペア間の力を計算するカーネルの1つは、それぞれが500,000倍、合計3ミリオン倍(22 MB)の6つの配列を受け取ります。

したがって、30百万フロートポイントを配置する予定です。これは約114 MBのグローバルメモリなので、これは問題ではありません。私のラップトップでも250MBです。

あなたの場合、計算の数が問題になる可能性がありますか? Molecular Dynamic(MD)での私の経験に基づいて、私はノーと言います。順次MDバージョンは完了するのに約25時間かかりますが、GPUでは45分かかりました。あなたは、あなたのアプリケーションが数時間かかったと言いました、あなたのコード例に基づいて、それは分子動力学よりも柔らかく見えます。

力の計算例を次に示します。

__global__ void add(double *fx, double *fy, double *fz,
                    double *x, double *y, double *z,...){

     int pos = (threadIdx.x + blockIdx.x * blockDim.x); 

     ...

     while(pos < particles)
     {

      for (i = 0; i < particles; i++)
      {
              if(//inside of the same radius)
                {
                 // calculate force
                } 
       }
     pos += blockDim.x * gridDim.x;  
     }        
  }

Cudaのコードの簡単な例は、2つの2D配列の合計です。

Cで:

for(int i = 0; i < N; i++)
    c[i] = a[i] + b[i]; 

Cudaの場合:

__global__ add(int *c, int *a, int*b, int N)
{
  int pos = (threadIdx.x + blockIdx.x)
  for(; i < N; pos +=blockDim.x)
      c[pos] = a[pos] + b[pos];
}

Cudaでは、基本的にそれぞれを反復処理に使用し、各スレッドで分割します。

1) threadIdx.x + blockIdx.x*blockDim.x;

各ブロックには0〜N-1(Nはブロックの最大数)のIDがあり、各ブロックには0〜X-1のIDを持つX個のスレッドがあります。

1)各スレッドがそのidとスレッドが存在するブロックidに基づいて計算する反復を提供します。blockDim.xはブロックが持つスレッドの数です。

したがって、10個のスレッドとN = 40のブロックがそれぞれ2つある場合、次のようになります。

Thread 0 Block 0 will execute pos 0
Thread 1 Block 0 will execute pos 1
...
Thread 9 Block 0 will execute pos 9
Thread 0 Block 1 will execute pos 10
....
Thread 9 Block 1 will execute pos 19
Thread 0 Block 0 will execute pos 20
...
Thread 0 Block 1 will execute pos 30
Thread 9 Block 1 will execute pos 39

あなたのコードを見て、私はcudaで何ができるかのこのドラフトを作成しました:

__global__ hash (float *largeFloatingPointArray, int *dictionary)
    // You can turn the dictionary in one array of int
    // here each position will represent the float
    // Since  x = 0f; x < 100f; x += 0.0001f
    // you can associate each x to different position
    // in the dictionary:

    // pos 0 have the same meaning as 0f;
    // pos 1 means float 0.0001f
    // pos 2 means float 0.0002f ect.
    // Then you use the int of each position 
    // to count how many times that "float" had appeared 


   int x = blockIdx.x;  // Each block will take a different x to work
    float y;

while( x < 1000000) // x < 100f (for incremental step of 0.0001f)
{
    int noOfOccurrences = 0;
    float z = converting_int_to_float(x); // This function will convert the x to the
                                          // float like you use (x / 0.0001)

    // each thread of each block
    // will takes the y from the array of largeFloatingPointArray

    for(j = threadIdx.x; j < largeFloatingPointArraySize; j += blockDim.x)
    {
        y = largeFloatingPointArray[j];
        if (z == y)
        {
            noOfOccurrences++;
        }
    }
    if(threadIdx.x == 0) // Thread master will update the values
      atomicAdd(&dictionary[x], noOfOccurrences);
    __syncthreads();
}

異なるブロックの異なるスレッドが同時にnoOfOccurrencesを書き込み/読み取りする可能性があるため、atomicAddを使用する必要があります。そのため、相互排他を明確にする必要があります。

これは、ブロックではなくスレッドに外部ル​​ープの反復を与えることもできる唯一のアプローチです。

チュートリアル

Dr Dobbs Journalシリーズ CUDA:大衆向けのスーパーコンピューティング Rob Farmerの優れた作品で、14回の記事のほぼすべてを扱っています。また、かなり緩やかに開始されるため、初心者にも非常に適しています。

その他:

最後の項目を見てください。CUDAを学ぶための多くのリンクがあります。

OpenCL: OpenCLチュートリアル| MacResearch

78
dreamcrash

並列処理やGPGPUについてはあまり知りませんが、この特定の例では、100万回ループするのではなく、入力配列を1回パスすることで時間を大幅に節約できます。大規模なデータセットでは、可能な場合は通常、1回のパスで処理を行います。複数の独立した計算を実行している場合でも、同じデータセットを使用している場合は、同じパスですべてを実行する速度が向上する可能性があります。しかし、コードの複雑さが増すため、価値がないかもしれません。

さらに、そのように浮動小数点数に少量を繰り返し追加したくない場合、丸め誤差が加算され、意図したものが得られません。以下のサンプルにifステートメントを追加して、入力が反復のパターンに一致するかどうかを確認しますが、実際にそれが必要ない場合は省略します。

C#はわかりませんが、サンプルのシングルパス実装は次のようになります。

Dictionary<float, int> noOfNumbers = new Dictionary<float, int>();

foreach (float x in largeFloatingPointArray)
{
    if (math.Truncate(x/0.0001f)*0.0001f == x)
    {
        if (noOfNumbers.ContainsKey(x))
            noOfNumbers.Add(x, noOfNumbers[x]+1);
        else
            noOfNumbers.Add(x, 1);
    }
}

お役に立てれば。

11
AlliedEnvy

GPUを使用してこのような計算を高速化することは可能ですか(また、意味がありますか)?

  • 確かに[〜#〜] yes [〜#〜]、この種のアルゴリズムは通常、大規模な処理の理想的な候補ですdata-parallelism GPU、 。

「はい」の場合:誰でもチュートリアルを知っているか、サンプルコードを入手していますか(プログラミング言語は関係ありません)?

  • GPGPU方式にしたい場合、2つの選択肢があります:[〜#〜] cuda [〜#〜]またはOpenCL

    CUDAは多くのツールで成熟していますが、NVidia GPU中心です。

    OpenCLは、NVidiaとAMD GPU、およびCPUで実行される標準です。したがって、あなたは本当にそれを好むべきです。

  • チュートリアルでは、Rob FarberによるCodeProjectの優れたシリーズがあります。 http://www.codeproject.com/Articles/Rob-Farber#Articles

  • 特定のユースケースでは、OpenCLを使用したヒストグラムのサンプルが多数あります(多くはイメージヒストグラムですが、原則は同じです)。

  • C#を使用するときは、OpenCL.NetまたはClooのようなバインディングを使用できます。

  • 配列が大きすぎてGPUメモリに格納できない場合は、配列をブロック分割して、各部分のOpenCLカーネルを簡単に再実行できます。

8
Pragmateek

「largerFloatingPointArray」値をメモリから取得する必要があるため、GPUを使用するのが適切かどうかはわかりません。私の理解では、GPUは自己完結型の計算により適しています。

この単一プロセスアプリケーションを多くのシステムで実行される分散アプリケーションに変え、アルゴリズムを微調整することで、利用可能なシステムの数に応じて物事が大幅に高速化されると思います。

従来の「分割統治」アプローチを使用できます。私が取る一般的なアプローチは次のとおりです。

1つのシステムを使用して、 'largeFloatingPointArray'をハッシュテーブルまたはデータベースにプリプロセスします。これは単一のパスで行われます。キーとして浮動小数点値を使用し、値として配列内の出現回数を使用します。最悪のシナリオは、各値が1回しか発生しないことですが、それはほとんどありません。 largeFloatingPointArrayがアプリケーションが実行されるたびに変化し続ける場合、インメモリハッシュテーブルは意味があります。静的な場合、テーブルはBerkeley DBなどのキーと値のデータベースに保存できます。これを「ルックアップ」システムと呼びましょう。

別のシステムでは、それを「メイン」と呼び、作業の塊を作成し、N個のシステムに作業項目を「ばらまき」、結果が利用可能になったときに「収集」します。たとえば、作業項目は、システムが動作する範囲を示す2つの数字のように単純な場合があります。システムが作業を完了すると、オカレンスの配列が送り返され、別の作業を処理する準備が整います。

LargeFloatingPointArrayを繰り返し処理しないため、パフォーマンスが向上します。ルックアップシステムがボトルネックになった場合、必要な数のシステムに複製できます。

十分な数のシステムが並行して動作しているため、処理時間を数分にまで短縮できるはずです。

私は、システム内の複数の「システムオンチップ」モジュールを使用して構築される/またはマイクロサーバーと呼ばれるメニーコアベースのシステムを対象としたC言語の並列プログラミング用のコンパイラに取り組んでいます。 ARMモジュールベンダーにはCalxeda、AMD、AMCCなどが含まれます。Intelもおそらく同様の製品を提供します。

このようなアプリケーションに使用できるコンパイラのバージョンが動作しています。コンパイラは、C関数プロトタイプに基づいて、システム間でプロセス間通信コード(IPC)を実装するCネットワークコードを生成します。利用可能なIPCメカニズムの1つはsocket/tcp/ipです。

分散ソリューションの実装に支援が必要な場合は、喜んで議論します。

2012年11月16日に追加。

私はアルゴリズムについてもう少し考えました、そして、これはシングルパスでそれをするべきだと思います。それはCで書かれており、あなたが持っているものと比較して非常に高速であるべきです。

/*
 * Convert the X range from 0f to 100f in steps of 0.0001f
 * into a range of integers 0 to 1 + (100 * 10000) to use as an
 * index into an array.
 */

#define X_MAX           (1 + (100 * 10000))

/*
 * Number of floats in largeFloatingPointArray needs to be defined
 * below to be whatever your value is.
 */

#define LARGE_ARRAY_MAX (1000)

main()
{
    int j, y, *noOfOccurances;
    float *largeFloatingPointArray;

    /*
     * Allocate memory for largeFloatingPointArray and populate it.
     */

    largeFloatingPointArray = (float *)malloc(LARGE_ARRAY_MAX * sizeof(float));    
    if (largeFloatingPointArray == 0) {
        printf("out of memory\n");
        exit(1);
    }

    /*
     * Allocate memory to hold noOfOccurances. The index/10000 is the
     * the floating point number.  The contents is the count.
     *
     * E.g. noOfOccurances[12345] = 20, means 1.2345f occurs 20 times
     * in largeFloatingPointArray.
     */

    noOfOccurances = (int *)calloc(X_MAX, sizeof(int));
    if (noOfOccurances == 0) {  
        printf("out of memory\n");
        exit(1);
    }

    for (j = 0; j < LARGE_ARRAY_MAX; j++) {
        y = (int)(largeFloatingPointArray[j] * 10000);
        if (y >= 0 && y <= X_MAX) {
            noOfOccurances[y]++;
        }   
    }
}
6
Arun Taylor

上記のポスターによる提案に加えて、適切な場合はTPL(タスク並列ライブラリ)を使用して、複数のコアで並列に実行します。

上記の例ではParallel.ForeachとConcurrentDictionaryを使用できますが、配列がチャンクに分割され、それぞれが単一の辞書に削減される辞書を生成するより複雑なmap-reduceセットアップがより良い結果をもたらします。

すべての計算がGPU機能に正しくマッピングされるかどうかはわかりませんが、いずれにしてもmap-reduceアルゴリズムを使用して計算をGPUコアにマッピングし、部分的な結果を単一の結果に減らす必要があります。あまり馴染みのないプラットフォームに移る前に、CPUでそれを行うこともできます。

6
Eli Algranti