GPUコンピューティングにはどのような問題が役立ちますか?


84

だから、私が取り組んでいる問題がシリアルで最良のものであり、それを並行して管理できるものについて、まともな頭を持っています。しかし、現時点では、CPUベースの計算によって何が最適に処理され、GPUに何をオフロードする必要があるのか​​についてはあまり知りません。

私はその基本的な質問を知っていますが、私の検索の多くは、なぜ、またはやや漠然とした経験則を正当化することなく、どちら一方を明確に主張する人々に巻き込まれます。ここでより有用な応答を探しています。

回答:


63

GPUハードウェアには、rawコンピューティング(FLOP)とメモリ帯域幅という2つの特定の長所があります。最も難しい計算上の問題は、これらの2つのカテゴリのいずれかに分類されます。たとえば、密な線形代数(A * B = CまたはSolve [Ax = y]またはDiagonalize [A]など)は、システムサイズに応じて計算/メモリ帯域幅スペクトルのどこかに分類されます。高速フーリエ変換(FFT)も、この型に適合し、高い帯域幅のニーズに対応します。他の変換、グリッド/メッシュベースのアルゴリズム、モンテカルロなどと同様に、NVIDIA SDKのコード例を見ると、最も一般的に対処されている種類の問題を感じることができます。

より有益な答えは、「GPUが本当に悪いのはどのような問題ですか?」という質問に対する答えだと思います。このカテゴリに分類されないほとんどの問題は、GPUで実行することができますが、他の問題は他の問題よりも手間がかかります。

うまくマッピングできない問題は、一般的に小さすぎるか、予測不可能です。非常に小さな問題には、GPUのすべてのスレッドを使用するために必要な並列性が欠けているか、CPUの低レベルキャッシュに収まり、CPUパフォーマンスが大幅に向上します。予測不可能な問題には意味のある分岐が多すぎるため、GPUメモリからコアへのデータの効率的なストリーミングを妨げたり、SIMDパラダイムを破って並列性を低下させたりする可能性があります(「分岐ワープ」を参照)。これらの種類の問題の例は次のとおりです。

  • ほとんどのグラフアルゴリズム(特にメモリ空間では予測不能)
  • スパース線形代数(ただし、これはCPUにとっても悪い)
  • 小信号処理の問題(たとえば、1000ポイント未満のFFT)
  • 調べる
  • ソート

3
それでも、これらの「予測不可能な」問題に対するGPUソリューション可能であり、今日では一般的に実行可能ではありませんが、将来的には重要になる可能性があります。
左辺約

6
GPUパフォーマンスブレーカーのリストに特にブランチを追加したいと思います。すべての(数百)が(SIMDのように)同じ命令を実行して、真の並列計算を実行したい場合。たとえば、AMDカードでは、命令フローのいずれかが分岐に遭遇して分岐する必要がある場合、すべての波面(並列グループ)が分岐します。波面からの別のユニットが発散してはならない場合-彼らは2番目のパスを実行する必要があります。それが、予測可能性によってmaxhutchが意味することです。
バイオレットキリン

2
@VioletGiraffe、それは必ずしも真実ではありません。CUDA(Nvidia GPUなど)では、分岐の分岐は現在のワープ(最大32スレッド)にのみ影響します。異なるワープは、同じコードを実行しますが、明示的に同期しない限り(たとえばと__synchtreads())同期しません。
ペドロ

1
@Pedro:本当ですが、一般的に分岐するパフォーマンスが低下します。高性能コード(GPUコードではないもの)の場合、それを考慮することはほぼ不可欠です。
jvriesem

21

高い演算強度と通常のメモリアクセスパターンを持つ問題は、通常、GPUに実装するのが簡単であり、それらでうまく機能します。

高性能GPUコードを作成する際の基本的な難しさは、大量のコアがあり、それらを可能な限り最大限に活用することです。不規則なメモリアクセスパターンを持っているか、高い演算強度を持たない問題は、これを困難にします:結果の通信に長い時間を費やすか、メモリからデータをフェッチするのに長い時間を費やします(遅い!)もちろん、コードの同時実行の可能性は、GPUでも適切に実装される能力にとって重要です。


通常のメモリアクセスパターンの意味を指定できますか?
フォマイト

1
maxhutchの答えは私のものよりも優れています。通常のアクセスパターンが意味することは、メモリは時間的にも空間的にもローカルにアクセスされるということです。つまり、メモリを何度も繰り返しジャンプすることはありません。それはまた、私が気づいたパッケージ取引のようなものです。また、分岐(コード内の条件ステートメント)を最小限に抑えるために、コンパイラーまたはプログラマーがデータアクセスパターンを事前に決定できることも意味します。
Reid.Atcheson

15

これは、それ自体での答えとしてではなく、maxhutchReid.Atchesonによる他の答えへの追加として意図されています。

GPUを最大限に活用するには、問題を高度(または大規模)に並列化する必要があるだけでなく、GPUで実行されるコアアルゴリズムもできるだけ小さくする必要があります。OpenCL用語これは、主と呼ばれているカーネル

より正確には、カーネルはGPU の各マルチプロセッシングユニット(またはコンピューティングユニット)のレジスタに収まる必要があります。レジスタの正確なサイズはGPUに依存します。

カーネルが十分に小さい場合、問題の生データはGPUのローカルメモリに収まる必要があります(読み取り:ローカルメモリ(OpenCL)または計算ユニットの共有メモリ(CUDA))。そうしないと、GPUの高いメモリ帯域幅でさえ、処理要素を常にビジー状態に保つのに十分な速度ではありません。
通常、このメモリは約16〜32 KiByteある


各処理ユニットのローカル/共有メモリは、コアの単一クラスター内で実行されている多数のスレッドすべてで共有されていませんか?この場合、GPUから最大限のパフォーマンスを引き出すために、実際にデータのワーキングセットを大幅に小さくする必要はありませんか?
ダン・ニーリー

処理ユニットのローカル/共有メモリは、計算ユニット自体のみがアクセスできるため、この計算ユニットの処理要素のみが共有します。グラフィックスカードのグローバルメモリ(通常1GB)は、すべての処理ユニットからアクセス可能です。処理要素とローカル/共有メモリ間の帯域幅は非常に高速(> 1TB / s)ですが、グローバルメモリへの帯域幅はかなり遅く(〜100GB / s)、すべての計算ユニット間で共有する必要があります。
Torbjörn

メインのGPUメモリについては聞いていませんでした。オンダイメモリは、個々のコアごとではなく、コアレベルのクラスターでのみ割り当てられると思いました。nVidia GF100 / 110 gpuの場合; 512個のcudaコアではなく、16個のSMクラスターのそれぞれに対して。各SMで最大32スレッドを並行して実行するように設計されているため、GPUパフォーマンスを最大化するには、ワーキングセットを1kb /スレッドの範囲に維持する必要があります。
ダン・ニーリー

@TorbjoernすべてのGPU実行パイプラインをビジーに保つことは、GPUがこの2つの方法を実現することです:(1)最も一般的な方法は、同時スレッドの数を増やすことで占有率を増やすか、別の言い方をすることです(小さなカーネルはより多くのアクティブなスレッドを持つことができるように共有リソース); (2)カーネル内の命令レベルの並列処理を増やすことで、比較的低い占有率(アクティブなスレッド数が少ない)でより大きなカーネルを実現できます。参照してくださいbit.ly/Q3KdI0
fcruz

11

おそらく以前の回答に対するより技術的な追加:CUDA(つまりNvidia)GPUは、それぞれ32スレッドで自律的に動作するプロセッサのセットとして説明できます。各プロセッサーのスレッドはロックステップで動作します(長さ32のベクトルを持つSIMDを考えてください)。

GPUを使用する最も魅力的な方法は、絶対にすべてがロックステップで実行されるふりをすることですが、これが常に最も効率的な方法とは限りません。

コードが数百/数千のスレッドにうまく/自動的に並列化されていない場合、それを個々の非同期タスクに分割して、並列化をうまく行い、ロックステップで実行している32スレッドのみでそれらを実行できる場合があります。CUDAは、mutexの実装を可能にするアトミック命令のセットを提供します。これにより、プロセッサは相互に同期し、スレッドプールパラダイムでタスクのリストを処理できます。コードは、マルチコアシステムと同じように機能します。各コアには32個のスレッドがあることに注意してください。

CUDAを使用した、これがどのように機能するかの小さな例を次に示します。

/* Global index of the next available task, assume this has been set to
   zero before spawning the kernel. */
__device__ int next_task;

/* We will use this value as our mutex variable. Assume it has been set to
   zero before spawning the kernel. */
__device__ int tasks_mutex;

/* Mutex routines using atomic compare-and-set. */
__device__ inline void cuda_mutex_lock ( int *m ) {
    while ( atomicCAS( m , 0 , 1 ) != 0 );
    }
__device__ inline void cuda_mutex_unlock ( int *m ) {
    atomicExch( m , 0 );
    }

__device__ void task_do ( struct task *t ) {

    /* Do whatever needs to be done for the task t using the 32 threads of
       a single warp. */
    }

__global__ void main ( struct task *tasks , int nr_tasks ) {

    __shared__ task_id;

    /* Main task loop... */
    while ( next_task < nr_tasks ) {

        /* The first thread in this block is responsible for picking-up a task. */
        if ( threadIdx.x == 0 ) {

            /* Get a hold of the task mutex. */
            cuda_mutex_lock( &tasks_mutex );

            /* Store the next task in the shared task_id variable so that all
               threads in this warp can see it. */
            task_id = next_task;

            /* Increase the task counter. */
            next_tast += 1;

            /* Make sure those last two writes to local and global memory can
               be seen by everybody. */
            __threadfence();

            /* Unlock the task mutex. */
            cuda_mutex_unlock( &tasks_mutex );

            }

        /* As of here, all threads in this warp are back in sync, so if we
           got a valid task, perform it. */
        if ( task_id < nr_tasks )
            task_do( &tasks[ task_id ] );

        } /* main loop. */

    }

次に、カーネルを呼び出して、main<<<N,32>>>(tasks,nr_tasks)各ブロックに32スレッドのみが含まれ、単一のワープに収まるようにする必要があります。この例では、簡単にするために、タスクには依存関係(たとえば、1つのタスクが別のタスクの結果に依存する)や競合(たとえば、同じグローバルメモリでの作業)がないと仮定しました。この場合、タスクの選択はもう少し複雑になりますが、構造は基本的に同じです。

もちろん、これはセルの1つの大きなバッチですべてを行うよりも複雑ですが、GPUを使用できる問題のタイプを大幅に拡大します。


2
これは技術的には事実ですが、高いメモリ帯域幅を得るには高い並列性が必要であり、非同期カーネル呼び出しの数には制限があります(現在は16)。また、現在のリリースでのスケジューリングに関連する文書化されていない動作が大量にあります。私は...当分のパフォーマンスをimporoveする非同期カーネルに頼ることに対して助言するだろう
マックス・ハッチンソン

2
私が説明していることは、すべて単一のカーネル呼び出しで実行できます。各ブロックが1つのワープに収まるように、それぞれ32スレッドのNブロックを作成できます。次に、各ブロックは、グローバルタスクリスト(アトミック/ミューテックスを使用してアクセス制御)からタスクを取得し、32のロックステップスレッドを使用して計算します。これはすべて、単一のカーネル呼び出しで発生します。コード例が必要な場合はお知らせください。投稿します。
ペドロ

4

これまでになされていない点の1つは、GPUの現在の世代は、単精度の計算の場合と同様に、倍精度の浮動小数点の計算でもうまくいかないことです。計算を倍精度で行う必要がある場合、実行時間は単精度よりも10倍ほど長くなると予想できます。


私は反対したい。ほとんど(またはすべて)の新しいGPUには、ネイティブの倍精度サポートがあります。ほぼすべてのそのようなGPUは、おそらく必要なメモリアクセス/帯域幅の単純な倍増のために、単精度の約半分の速度で実行される倍精度の計算を報告します。
ゴドリックシーア

1
最新かつ最高のNvidia Teslaカードは、ピークの単精度性能の半分であるピークの倍精度性能を提供するのは事実ですが、より一般的なFermiアーキテクチャの消費者グレードのカードの比率は8対1です。
ブライアンボーチャーズ

@GodricSeer SPとDP浮動小数点の2:1の比率は、帯域幅とはほとんど関係がなく、これらの操作を実行するために存在するハードウェアユニットの数とほとんど関係があります。SPおよびDPのレジスタファイルを再利用するのが一般的であるため、浮動小数点ユニットはSP操作の2倍をDP操作として実行できます。この設計には多くの例外があります。たとえば、IBM Blue Gene / Q(SPロジックがないため、SPは約1.05x DPで実行されます)。一部のGPUには、2以外の比率(3と5など)があります
ジェフ

この回答を書いてから4年が経ち、NVIDIA GPUの現在の状況では、GeForceおよびQuadroラインのDP / SP比は1/32になっています。NVIDIAのTesla GPUは、はるかに強力な倍精度パフォーマンスを備えていますが、コストも高くなります。一方、AMDはRadeon GPUで同じように倍精度のパフォーマンスを損なうことはありません。
ブライアンボー

4

比phor的な観点から、GPUは爪のベッドに横たわっている人として見ることができます。一番上に横たわっている人がデータであり、各爪の付け根にはプロセッサがあります。そのため、爪は実際にはプロセッサからメモリを指す矢印です。すべての爪は、グリッドのような規則的なパターンです。体が十分に広がっている場合、気分が良い(パフォーマンスが良い)場合、体が爪床のいくつかのスポットのみに触れる場合、痛みは悪いです(パフォーマンスが悪い)。

これは、上記の優れた回答に対する補完的な回答と見なすことができます。


4

古い質問ですが、統計手法に関連する2014年のこの回答は、ループが何であるかを知っているすべての人にとって一般化可能であると思いますが、特に説明的で有益です。


2

GPUのI / Oレイテンシは長いため、メモリを飽和させるには多くのスレッドを使用する必要があります。ワープをビジー状態に保つには、多くのスレッドが必要です。コードパスが10クロックでI / Oレイテンシが320クロックの場合、32スレッドがワープを飽和状態に近づけるはずです。コードパスが5クロックの場合、スレッドを2倍にします。

コアが1000ある場合、GPUを完全に活用するために数千のスレッドを探します。

メモリアクセスはキャッシュラインで、通常は32バイトです。1バイトをロードすると、コストは32バイトに匹敵します。そのため、ストレージを結合して、使用の局所性を高めます。

各ワープには多数のレジスタとローカルRAMがあり、隣接共有が可能です。

大規模なセットの近接シミュレーションは、最適化する必要があります。

ランダムI / Oとシングルスレッドは、とても楽しい...


これは本当に魅力的な質問です。私は、各タスクが〜0.06秒かかるが実行する〜180万タスクがある場合に、合理的な単純なタスク(空中画像でのエッジ検出)を「並列化」することが可能か(または努力する価値があるか)について議論しています毎年、6年分のデータ:タスクは確実に分離可能です)...したがって、1つのコアで〜7.5日分の計算時間に相当します。GPUで各計算が高速で、ジョブをnGPUコアごとに1並列化できる場合[n小]、実際にジョブ時間は1時間まで短縮される可能性がありますか?ありそうもない。
GT。

0

トラベリングセールスマンのような多くのブルートフォースによって解決できる問題を想像してください。次に、それぞれ8枚のスパンキービデオカードを備えたサーバーラックがあり、各カードに3000個のCUDAコアがあることを想像してください。

すべての可能なセールスマンのルートを解決し、時間/距離/何らかのメトリックでソートします。作業のほぼ100%を破棄していることは確かですが、場合によってはブルートフォースが実行可能なソリューションです。


1週間、このようなサーバーが4台ある小規模なファームにアクセスできましたが、5日間で10年前より多くのdistributed.netブロックを実行しました。
クリギー

-1

多くのエンジニアリングのアイデアを研究することから、私は、gpuはタスク、メモリ管理、反復可能な計算のタスクに焦点を合わせた形だと思います。

多くの数式は簡単に記述できますが、マトリックス数学のように単一の答えは得られず、多くの値が得られるなど、計算するのは苦痛です。

一部の数式はすべての計算値なしでは実行できないため、コンピューターが値を計算し、数式を実行する速度として計算で重要になります(したがって、速度が低下します)。コンピューターは、これらのプログラムで使用する式の実行順序や値の計算順序をよく知りません。主に高速で力ずくで計算し、式をチャックに分割して計算しますが、最近の多くのプログラムでは、これらの計算されたチャックを今すぐ必要とし、キュー(およびキューのキューおよびキューのキュー)で待機します。

例えば、衝突で最初に計算されるべきシミュレーションゲームでは、衝突の損傷、オブジェクトの位置、新しい速度は?これにはどれくらい時間がかかりますか?どのCPUもこの負荷をどのように処理できますか?また、ほとんどのプログラムは非常に抽象的であり、データの処理により多くの時間を必要とし、常にマルチスレッド用に設計されているわけではなく、抽象プログラムでこれを効果的に行う方法もありません。

CPUの性能が向上し、優れた人々がプログラミングでだらしないようになり、さまざまな種類のコンピューター用にプログラミングする必要があります。GPUは、多くの単純な計算を同時にブルートフォースするように設計されています(メモリ(セカンダリ/ RAM)はもちろんのこと、コンピューティングの主なボトルネックは加熱冷却です)。CPUは、同時に多くの多くのキューを管理している、または多くの方向に引き寄せられて、何ができないのかを考えています。(ちょっとそれはほとんど人間です)

gpuは退屈な作業の退屈な労働者です。CPUは完全なカオスを管理しており、すべての詳細を処理することはできません。

それで、私たちは何を学びますか?GPUは詳細な退屈な作業を一度に実行し、CPUはマルチタスクマシンであり、実行するタスクが多すぎるとあまり焦点を合わせることができません。(注意障害と自閉症を同時に持っているようです)。

エンジニアリングには、アイデア、デザイン、現実、そして多くの単調な仕事があります。

簡単に始めて、すぐに始め、すぐに失敗し、すぐに失敗することを忘れないでください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.