Thrustを使ってCUDAで最近傍法を実装してみた(サンプルコードあり)

この記事では最近傍法(nearest neighbor algorithm)をCUDAで実装する場合のサンプルを紹介しています。Thrustというライブラリを用いることで、GPUコンピューティング、つまりCUDAでのプログラミングが非常に簡単になります。

そこで、Thrustを使ったCUDAのプログラムを紹介するうえで最近傍法の計算部分(距離の計算)を実装してみました。

Thrustを使わない場合のCUDAの最近傍法

注目すべきポイントはdeviceのメモリの管理の部分です。メモリ管理をcudaMallocやcudaMemcpyを使用しなければいけない点です。この処理は直感的ではないため、CUDAについて知らない人が見たときに少しわかりづらいです。また、メモリ確保を怠るとエラーの原因になってしまうため、面倒です。

そこで、Thrustを使うことでメモリ管理が楽になります。

#include<cuda_runtime.h>
#include <device_launch_parameters.h>
#include <thrust/host_vector.h>
#include <thrust/device_vector.h>
#include <thrust/execution_policy.h>
#define M 5 //点数 m

__global__ void calcNearestNNGpu(double* x1, double* y1, double* x2, double* y2, double* distance, double* dx, double* dy)
{
       int threadNumX = blockIdx.x * blockDim.x + threadIdx.x;
       if (threadNumX < M){
              dx[threadNumX] = x2[threadNumX] - x1[0];
              dy[threadNumX] = y2[threadNumX] - y1[0];
              distance[threadNumX] = sqrt(pow(dx[threadNumX], 2) + pow(dy[threadNumX], 2));
       }
}
int main(){
       //hostの変数
       double x1[M]; double x2[M]; double y1[M]; double y2[M];
       double dx[M]; double dy[M];
       double distance[M];

       //deviceの変数
       double *x1_d; double *x2_d; double *y1_d; double *y2_d;
       double *dx_d; double *dy_d;
       double *distance_d;

       //deviceのメモリの管理
       int devSize = sizeof(double)*M;
       cudaMalloc((void**)&x1_d, devSize);
       cudaMalloc((void**)&x2_d, devSize);
       cudaMalloc((void**)&y1_d, devSize);
       cudaMalloc((void**)&y2_d, devSize);
       cudaMalloc((void**)&dx_d, devSize);
       cudaMalloc((void**)&dy_d, devSize);
       cudaMalloc((void**)&distance_d, devSize);
       cudaMemcpy(x1_d, x1, devSize, cudaMemcpyHostToDevice);
       cudaMemcpy(x2_d, x2, devSize, cudaMemcpyHostToDevice);
       cudaMemcpy(y1_d, y1, devSize, cudaMemcpyHostToDevice);
       cudaMemcpy(y2_d, y2, devSize, cudaMemcpyHostToDevice);

       //ホスト上の変数の初期化
       for (int initNum = 0; initNum < M; initNum++){
              x1[initNum ] = initNum;
              x2[initNum ] = initNum;
              y1[initNum ] = initNum;
              y2[initNum ] = initNum;

              std::cout << "x1: " << initNum << "番目:  " << x1[initNum] << std::endl;
              std::cout << "x2: " << initNum << "番目:  " << x2[initNum] << std::endl;
       }

       const dim3 block(5, 1);
       const dim3 grid(1, 1);
       //kernelを実行
       calcNearestNNGpu << <grid, block >> > (thrust::raw_pointer_cast(&x1_d[0])
              , thrust::raw_pointer_cast(&y1_d[0]), thrust::raw_pointer_cast(&x2_d[0])
              , thrust::raw_pointer_cast(&y2_d[0]), thrust::raw_pointer_cast(&distance_d[0])
              , thrust::raw_pointer_cast(&dx_d[0]), thrust::raw_pointer_cast(&dy_d[0])
              );
       cudaDeviceSynchronize;
       //距離を代入
       cudaMemcpy(distance, distance_d, devSize, cudaMemcpyDeviceToHost);
       //確認用 距離を表示
       for (int j = 0; j < M; j++){
              std::cout << "distance: " << j << "番目:  " << distance[j] << std::endl;
       }
}

Thrustを使う場合のCUDAの最近傍法

Thrustを使う場合、変数の宣言は長くなりますが、Device側(GPU)にHost側(CPU)の値を代入することが簡単になります。 Device側の変数には全て「_d」を付けていますが、x1_dにHost側のx1を代入するだけでDevice側にデータをコピーすることが完了します。

そのため、非常に直感的です。
ただし、1つだけ問題があって、デバッグがしづらいということです。ブレークポイントを設定しても値の参照ができないため、デバッグがしづらいです。 そのため、一々printfなどで値を直接コンソール上に表示したりする必要があります。

#include<cuda_runtime.h>
#include <device_launch_parameters.h>
#include <thrust/host_vector.h>
#include <thrust/device_vector.h>
#include <thrust/execution_policy.h>
#define M 5 //点数 m

__global__ void calcNearestNNGpu(double* x1, double* y1, double* x2, double* y2, double* distance, double* dx, double* dy)
{
       int threadNumX = blockIdx.x * blockDim.x + threadIdx.x;
       if (threadNumX < M){
              dx[threadNumX] = x2[threadNumX] - x1[0];
              dy[threadNumX] = y2[threadNumX] - y1[0];
              distance[threadNumX] = sqrt(pow(dx[threadNumX], 2) + pow(dy[threadNumX], 2));
       }
}
int main(){
       //ホスト上のメモリ定義
       thrust::host_vector<double> x1(M);
       thrust::host_vector<double> y1(M);
       thrust::host_vector<double> x2(M);
       thrust::host_vector<double> y2(M);
       thrust::host_vector<double> distance(M);
       thrust::host_vector<double> dx(M);
       thrust::host_vector<double> dy(M);
       //デバイス上のメモリ定義
       thrust::device_vector<double> x1_d(M);
       thrust::device_vector<double> y1_d(M);
       thrust::device_vector<double> x2_d(M);
       thrust::device_vector<double> y2_d(M);
       thrust::device_vector<double> distance_d(M);
       thrust::device_vector<double> dx_d(M);
       thrust::device_vector<double> dy_d(M);

       //host側のデータを1,2,3,4,5で初期化 あらかじめdevice側を初期化してもOK
       thrust::sequence(x1.begin(), x1.end());
       thrust::sequence(y1.begin(), y1.end());
       thrust::sequence(x2.begin(), x2.end());
       thrust::sequence(y2.begin(), y2.end());

       //ホスト上の変数の初期化
       for (int initNum = 0; initNum < M; initNum++){
              std::cout << "x1: " << initNum << "番目:  " << x1[initNum] << std::endl;
              std::cout << "x2: " << initNum << "番目:  " << x2[initNum] << std::endl;
       }
       //デバイスにホストの変数のコピー
       x1_d = x1;
       y1_d = y1;
       x2_d = x2;
       y2_d = y2;
       dx_d = dx;
       dy_d = dy;
       const dim3 block(5, 1);
       const dim3 grid(1, 1);
       //kernelを実行
       calcNearestNNGpu << <grid, block >> > (thrust::raw_pointer_cast(&x1_d[0])
              , thrust::raw_pointer_cast(&y1_d[0]), thrust::raw_pointer_cast(&x2_d[0])
              , thrust::raw_pointer_cast(&y2_d[0]), thrust::raw_pointer_cast(&distance_d[0])
              , thrust::raw_pointer_cast(&dx_d[0]), thrust::raw_pointer_cast(&dy_d[0])
              );
       cudaDeviceSynchronize;
       //距離を代入
       distance = distance_d;
       //確認用 距離を表示
       for (int j = 0; j < M; j++){
              std::cout << "distance: " << j << "番目:  " << distance[j] << std::endl;
       }
}

最近傍法におけるThrust部分のまとめ

       //ホスト上のメモリ定義
       thrust::host_vector<double> x1(M);
       //デバイス上のメモリ定義
       thrust::device_vector<double> x1_d(M);
       //host側のデータを1,2,3,4,5で初期化 あらかじめdevice側を初期化してもOK
       thrust::sequence(x1.begin(), x1.end());
       //デバイスにホストの変数のコピー
       x1_d = x1;
//kernelを実行
       calcNearestNNGpu << <grid, block >> > (thrust::raw_pointer_cast(&x1_d[0])
              , thrust::raw_pointer_cast(&y1_d[0]), thrust::raw_pointer_cast(&x2_d[0])
              , thrust::raw_pointer_cast(&y2_d[0]), thrust::raw_pointer_cast(&distance_d[0])
              , thrust::raw_pointer_cast(&dx_d[0]), thrust::raw_pointer_cast(&dy_d[0])
              );
       //距離を代入
       distance = distance_d;

まとめ

Thrustを利用すると、問題点はありますが、非常にプログラミングが楽になります。 そのため、必要に応じて使用してみるといいでしょう。Thrust自体に最小値や最大値を求める機能もあるため、最近傍法との相性も抜群です。 GPUコンピューティングでは最小値を求める処理を自分で書くのは難しいですが、Thrustを使えば簡単です。

今回は距離の計算部分のみでしたが、Thrustを使うことで最小値の計算を1行で終わらせることができます。