CUDAでプログラミングする前にやっておきたいこと
こちらの記事もご確認下さい。
CUDA6.0用にCPUとGPUの速度比較コードを修正 - 株式会社CFlatの明後日スタイルのブログ
GPUとCPUの速度比較をしたい
以前CUDAでのプログラミングが完成した際に、CPUとの速度比較を行いたいという当たり前の要望が上がりました。 そこでGPUとCPUを切り替えるコードを作成したのですが、どうせなら最初から作っておくべきだったと反省しました。 今回はその際に作成したGPUとCPUの速度比較コードを公開します。
OpenMPの設定
GPUとCPUの速度比較をする際にCPUの方も並列化しないと不公平です。CPUの並列化には設定が簡単なOpenMPを使います。 CUDAプロジェクトでOpenMPを使用できるようにするには、プロジェクトのプロパティを開き、構成プロパティ/CUDA C/C++/Command Lineの追加オプションに-Xcompiler "/openmp"と記載します。 あとは並列化したいfor文の前に#pragma omp parallel forを付け加えるだけです。 CUDAでのプログラミングが初めての方はこの記事も参考にして下さい。
GPUとCPUの切り替えを行うラッピング
下記にあるSwitchableCPUGPU.cuhとTimer.cuh内で宣言されたマクロを呼ぶと、USE_GPUの切り替えでGPUとCPUを切り替えることができます。
SwitchableCPUGPU.cuh
#pragma once #include "Timer.cuh" #define USE_GPU #ifdef USE_GPU #define SWITCHABLE_DEVICE __device__ #define SWITCHABLE_GLOBAL __global__ #define SWITCHABLE_TIMER CudaEventTimer #else #define SWITCHABLE_DEVICE #define SWITCHABLE_GLOBAL #define SWITCHABLE_TIMER Timer #endif template <typename T> void SwitchableCudaMalloc( T& val, int size ) { #ifdef USE_GPU cudaMalloc( (void**)&val, sizeof(T)*size ); #endif } template <typename T> void SwitchableCudaFree( T* val ) { #ifdef USE_GPU cudaFree( val ); #endif } template <typename T> void SwitchableCudaMemcpyHostToDevice( const T* const host, T* const device, int size ) { #ifdef USE_GPU cudaMemcpy( device, host, sizeof(T)*size, cudaMemcpyHostToDevice ); #endif } template <typename T> void SwitchableCudaMemcpyDeviceToHost( const T* const device, T* const host, int size ) { #ifdef USE_GPU cudaMemcpy( host, device, sizeof(T)*size, cudaMemcpyDeviceToHost); #endif }
Timer.cuh
#pragma once #include <string> #include <time.h> class CudaEventTimer { public : CudaEventTimer( const std::string& message ) : m_message( message ) { cudaEventCreate(&m_start); cudaEventCreate(&m_end); cudaEventRecord( m_start, 0 ); } ~CudaEventTimer() { cudaEventRecord( m_end, 0 ); cudaEventSynchronize( m_end ); float time; cudaEventElapsedTime( &time, m_start, m_end ); printf("%s = %f sec.\n",m_message.c_str(), time*0.001); cudaEventDestroy( m_start ); cudaEventDestroy( m_end ); } private: cudaEvent_t m_start; cudaEvent_t m_end; std::string m_message; }; class Timer { public : Timer( const std::string& message ) : m_message( message ) { m_start = clock(); } ~Timer() { m_end = clock(); printf("%s = %f sec.\n",m_message.c_str(), (double)(m_end - m_start)/CLOCKS_PER_SEC); } private: clock_t m_start; clock_t m_end; std::string m_message; };
行列計算でGPUとCPUを比較
下記が行列の掛け算を行うテストコードです。CUDA関連の処理をラッピングされた関数から呼ぶことによって、GPUとCPUの切り替えを容易にしています。
#include <stdio.h> #include "SwitchableCPUGPU.cuh" SWITCHABLE_GLOBAL void Calculate( float* matrixA, float* matrixB, float* matrixC, int iLength, int col = 0, int row = 0 ) { #ifdef USE_GPU row = blockIdx.x * blockDim.x + threadIdx.x; col = blockIdx.y * blockDim.y + threadIdx.y; if ( row > iLength || col > iLength ) return; #endif float target = 0.0f; for ( int i = 0 ; i < iLength ; ++i ) { target += matrixA[row*iLength + i] * matrixB[i*iLength + col]; } matrixC[row*iLength + col] = target; } int main() { // 行列のサイズ決定 const int iLength = 1024; const int iSize = iLength * iLength; // CPU側の変数初期化 float* matrixA = (float*)malloc(sizeof(float)*iSize); float* matrixB = (float*)malloc(sizeof(float)*iSize); float* matrixC = (float*)malloc(sizeof(float)*iSize); for ( int col = 0; col < iLength ; ++col ){ for ( int row = 0; row < iLength ; ++row ){ matrixA[col*iLength + row] = rand() % (1000); matrixB[col*iLength + row] = rand() % (1000); matrixC[col*iLength + row] = 0.0f; } } // ここから時間計測 SWITCHABLE_TIMER t("time"); // GPU側の変数初期化 float* d_matrixA; float* d_matrixB; float* d_matrixC; SwitchableCudaMalloc( d_matrixA, iSize ); SwitchableCudaMalloc( d_matrixB, iSize ); SwitchableCudaMalloc( d_matrixC, iSize ); SwitchableCudaMemcpyHostToDevice( matrixA, d_matrixA, iSize ); SwitchableCudaMemcpyHostToDevice( matrixB, d_matrixB, iSize ); // 行列計算 #ifdef USE_GPU const int iThread = 16; dim3 thread( iThread, iThread ); const int iBlock = ( iLength + iThread - 1 )/iThread; dim3 block( iBlock, iBlock ); Calculate<<<block, thread>>>( d_matrixA, d_matrixB, d_matrixC, iLength ); cudaThreadSynchronize(); #else #pragma omp parallel for for ( int i = 0 ; i < iLength ; ++i ) { for ( int j = 0 ; j < iLength ; ++j ) { Calculate( matrixA, matrixB, matrixC, iLength, i, j ); } } #endif // 後処理 SwitchableCudaMemcpyDeviceToHost( d_matrixC, matrixC, iSize ); free( matrixA ); free( matrixB ); free( matrixC ); SwitchableCudaFree( d_matrixA ); SwitchableCudaFree( d_matrixB ); SwitchableCudaFree( d_matrixC ); return 0; }
実行結果
実行結果は次のようになりました。参考までに並列化しなかったCPUの結果も載せておきます。 GPUには苦手な計算もあるので、OpenMPと比べて明らかに遅い場合はGPUの使用を検討した方が良いかもしれません。
GPU:0.152510s CPU OpenMP:1.220000s CPU 並列化無し:7.254000s
GPU計算の高速化
今回のプログラムでは全く行っていませんが、GPU計算には色々な高速化手法があります。 興味のある方は下記の書籍等を参考にして下さい。