回答:
GPUハードウェアには、rawコンピューティング(FLOP)とメモリ帯域幅という2つの特定の長所があります。最も難しい計算上の問題は、これらの2つのカテゴリのいずれかに分類されます。たとえば、密な線形代数(A * B = CまたはSolve [Ax = y]またはDiagonalize [A]など)は、システムサイズに応じて計算/メモリ帯域幅スペクトルのどこかに分類されます。高速フーリエ変換(FFT)も、この型に適合し、高い帯域幅のニーズに対応します。他の変換、グリッド/メッシュベースのアルゴリズム、モンテカルロなどと同様に、NVIDIA SDKのコード例を見ると、最も一般的に対処されている種類の問題を感じることができます。
より有益な答えは、「GPUが本当に悪いのはどのような問題ですか?」という質問に対する答えだと思います。このカテゴリに分類されないほとんどの問題は、GPUで実行することができますが、他の問題は他の問題よりも手間がかかります。
うまくマッピングできない問題は、一般的に小さすぎるか、予測不可能です。非常に小さな問題には、GPUのすべてのスレッドを使用するために必要な並列性が欠けているか、CPUの低レベルキャッシュに収まり、CPUパフォーマンスが大幅に向上します。予測不可能な問題には意味のある分岐が多すぎるため、GPUメモリからコアへのデータの効率的なストリーミングを妨げたり、SIMDパラダイムを破って並列性を低下させたりする可能性があります(「分岐ワープ」を参照)。これらの種類の問題の例は次のとおりです。
__synchtreads()
)同期しません。
高い演算強度と通常のメモリアクセスパターンを持つ問題は、通常、GPUに実装するのが簡単であり、それらでうまく機能します。
高性能GPUコードを作成する際の基本的な難しさは、大量のコアがあり、それらを可能な限り最大限に活用することです。不規則なメモリアクセスパターンを持っているか、高い演算強度を持たない問題は、これを困難にします:結果の通信に長い時間を費やすか、メモリからデータをフェッチするのに長い時間を費やします(遅い!)もちろん、コードの同時実行の可能性は、GPUでも適切に実装される能力にとって重要です。
これは、それ自体での答えとしてではなく、maxhutchとReid.Atchesonによる他の答えへの追加として意図されています。
GPUを最大限に活用するには、問題を高度(または大規模)に並列化する必要があるだけでなく、GPUで実行されるコアアルゴリズムもできるだけ小さくする必要があります。OpenCL用語これは、主と呼ばれているカーネル。
より正確には、カーネルはGPU の各マルチプロセッシングユニット(またはコンピューティングユニット)のレジスタに収まる必要があります。レジスタの正確なサイズはGPUに依存します。
カーネルが十分に小さい場合、問題の生データはGPUのローカルメモリに収まる必要があります(読み取り:ローカルメモリ(OpenCL)または計算ユニットの共有メモリ(CUDA))。そうしないと、GPUの高いメモリ帯域幅でさえ、処理要素を常にビジー状態に保つのに十分な速度ではありません。
通常、このメモリは約16〜32 KiByteある大。
おそらく以前の回答に対するより技術的な追加: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を使用できる問題のタイプを大幅に拡大します。
これまでになされていない点の1つは、GPUの現在の世代は、単精度の計算の場合と同様に、倍精度の浮動小数点の計算でもうまくいかないことです。計算を倍精度で行う必要がある場合、実行時間は単精度よりも10倍ほど長くなると予想できます。
古い質問ですが、統計手法に関連する2014年のこの回答は、ループが何であるかを知っているすべての人にとって一般化可能であると思いますが、特に説明的で有益です。
GPUのI / Oレイテンシは長いため、メモリを飽和させるには多くのスレッドを使用する必要があります。ワープをビジー状態に保つには、多くのスレッドが必要です。コードパスが10クロックでI / Oレイテンシが320クロックの場合、32スレッドがワープを飽和状態に近づけるはずです。コードパスが5クロックの場合、スレッドを2倍にします。
コアが1000ある場合、GPUを完全に活用するために数千のスレッドを探します。
メモリアクセスはキャッシュラインで、通常は32バイトです。1バイトをロードすると、コストは32バイトに匹敵します。そのため、ストレージを結合して、使用の局所性を高めます。
各ワープには多数のレジスタとローカルRAMがあり、隣接共有が可能です。
大規模なセットの近接シミュレーションは、最適化する必要があります。
ランダムI / Oとシングルスレッドは、とても楽しい...
トラベリングセールスマンのような多くのブルートフォースによって解決できる問題を想像してください。次に、それぞれ8枚のスパンキービデオカードを備えたサーバーラックがあり、各カードに3000個のCUDAコアがあることを想像してください。
すべての可能なセールスマンのルートを解決し、時間/距離/何らかのメトリックでソートします。作業のほぼ100%を破棄していることは確かですが、場合によってはブルートフォースが実行可能なソリューションです。
多くのエンジニアリングのアイデアを研究することから、私は、gpuはタスク、メモリ管理、反復可能な計算のタスクに焦点を合わせた形だと思います。
多くの数式は簡単に記述できますが、マトリックス数学のように単一の答えは得られず、多くの値が得られるなど、計算するのは苦痛です。
一部の数式はすべての計算値なしでは実行できないため、コンピューターが値を計算し、数式を実行する速度として計算で重要になります(したがって、速度が低下します)。コンピューターは、これらのプログラムで使用する式の実行順序や値の計算順序をよく知りません。主に高速で力ずくで計算し、式をチャックに分割して計算しますが、最近の多くのプログラムでは、これらの計算されたチャックを今すぐ必要とし、キュー(およびキューのキューおよびキューのキュー)で待機します。
例えば、衝突で最初に計算されるべきシミュレーションゲームでは、衝突の損傷、オブジェクトの位置、新しい速度は?これにはどれくらい時間がかかりますか?どのCPUもこの負荷をどのように処理できますか?また、ほとんどのプログラムは非常に抽象的であり、データの処理により多くの時間を必要とし、常にマルチスレッド用に設計されているわけではなく、抽象プログラムでこれを効果的に行う方法もありません。
CPUの性能が向上し、優れた人々がプログラミングでだらしないようになり、さまざまな種類のコンピューター用にプログラミングする必要があります。GPUは、多くの単純な計算を同時にブルートフォースするように設計されています(メモリ(セカンダリ/ RAM)はもちろんのこと、コンピューティングの主なボトルネックは加熱冷却です)。CPUは、同時に多くの多くのキューを管理している、または多くの方向に引き寄せられて、何ができないのかを考えています。(ちょっとそれはほとんど人間です)
gpuは退屈な作業の退屈な労働者です。CPUは完全なカオスを管理しており、すべての詳細を処理することはできません。
それで、私たちは何を学びますか?GPUは詳細な退屈な作業を一度に実行し、CPUはマルチタスクマシンであり、実行するタスクが多すぎるとあまり焦点を合わせることができません。(注意障害と自閉症を同時に持っているようです)。
エンジニアリングには、アイデア、デザイン、現実、そして多くの単調な仕事があります。
簡単に始めて、すぐに始め、すぐに失敗し、すぐに失敗することを忘れないでください。