4.5 CUDAによる高速化

4.5.1 CUDAプログラムの設計指針

CUDAによるFDTD法の高速化では下記の点に注意してプログラムを作成します。
ここではCUDA6以降のUnified Memoryについて説明します。

  1. 電磁界の更新は並列計算に適したアルゴリズムですので、自然な形で実装できます。
  2. 電磁界の各成分の更新ごとに1回のカーネルを起動します。
  3. 大きなセル数にも対応できるようにblockは(Z,Y)方向の2次元、 gridは(Z,Y,X)方向の3次元とします。
  4. GPUで参照する配列はcudaMallocManaged関数で確保し、cudaFree関数で解放します。 CPU/GPU間のメモリーコピーは不要です。

4.5.2 電磁界の更新

リスト4-5-1にEx成分を更新する関数を示します。
[3]の手法を用いてCPUコードとGPUコードを共通化しています。

ソースコードの説明
1行目:計算の実体である関数はCPUとGPUで共通です。 従って関数修飾子"__host__ __device__"を付けます。
12行目:GPUコードには関数修飾子"__global__"を付けます。
19-21行目:gridとblockからi,j,kを計算します。
23-25行目:i,j,kのオーバーフローをチェックします(セル数がブロックサイズの倍数とは限らないため)。
56-58行目:blockは内側から順にk,j,iとします。 アドレスはkについて連続ですのでこのようにする必要があります(coalescing of memory)。


リスト4-5-1 Ex成分の更新(updateEx.cu)
     1	__host__ __device__
     2	static void updateEx_(
     3		int64_t n, int64_t nj, int64_t nk,
     4		float *ex, float *hy, float *hz,
     5		float c1, float c2, float ryn, float rzn)
     6	{
     7		ex[n] = c1 * ex[n]
     8		      + c2 * (ryn * (hz[n] - hz[n - nj])
     9		            - rzn * (hy[n] - hy[n - nk]));
    10	}
    11	
    12	__global__
    13	static void updateEx_gpu(
    14		int nx, int ny, int nz,
    15		int64_t ni, int64_t nj, int64_t nk, int64_t n0,
    16		float *ex, float *hy, float *hz, unsigned char *iex,
    17		float *c1, float *c2, float *ryn, float *rzn)
    18	{
    19		int i = threadIdx.z + (blockIdx.z * blockDim.z);
    20		int j = threadIdx.y + (blockIdx.y * blockDim.y);
    21		int k = threadIdx.x + (blockIdx.x * blockDim.x);
    22	
    23		if ((i <  nx) &&
    24		    (j <= ny) &&
    25		    (k <= nz)) {
    26			int64_t n = (i * ni) + (j * nj) + (k * nk) + n0;
    27			updateEx_(
    28				n, nj, nk,
    29				ex, hy, hz,
    30				c1[iex[n]], c2[iex[n]], ryn[j], rzn[k]);
    31		}
    32	}
    33	
    34	static void updateEx_cpu(
    35		int nx, int ny, int nz,
    36		int64_t ni, int64_t nj, int64_t nk, int64_t n0,
    37		float *ex, float *hy, float *hz, unsigned char *iex,
    38		float *c1, float *c2, float *ryn, float *rzn)
    39	{
    40		for (int i = 0; i <  nx; i++) {
    41		for (int j = 0; j <= ny; j++) {
    42		for (int k = 0; k <= nz; k++) {
    43			int64_t n = (i * ni) + (j * nj) + (k * nk) + n0;
    44			updateEx_(
    45				n, nj, nk,
    46				ex, hy, hz,
    47				c1[iex[n]], c2[iex[n]], ryn[j], rzn[k]);
    48		}
    49		}
    50		}
    51	}
    52	
    53	void updateEx()
    54	{
    55		if (GPU) {
    56			dim3 grid(((Nz + 1) + (updateBlock.x - 1)) / updateBlock.x,
    57			          ((Ny + 1) + (updateBlock.y - 1)) / updateBlock.y,
    58			          ((Nx + 0) + (updateBlock.z - 1)) / updateBlock.z);
    59			updateEx_gpu<<<grid, updateBlock>>>(
    60				Nx, Ny, Nz,
    61				Ni, Nj, Nk, N0,
    62				Ex, Hy, Hz, iEx,
    63				C1, C2, RYn, RZn);
    64			cudaDeviceSynchronize();
    65		}
    66		else {
    67			updateEx_cpu(
    68				Nx, Ny, Nz,
    69				Ni, Nj, Nk, N0,
    70				Ex, Hy, Hz, iEx,
    71				C1, C2, RYn, RZn);
    72		}
    73	}

4.5.3 Mur吸収境界条件

Mur吸収境界条件はリスト4-5-2のようになります。
32,46,60行目のように1次元配列を1次元のblockとgridに分割しています。


リスト4-5-2 Mur吸収境界条件(murH_gpu.cu)
     1	__host__ __device__
     2	static void murh(float *h, mur2_t *q, param_t *p)
     3	{
     4		int64_t n0 = LA(p, q->i,  q->j,  q->k );
     5		int64_t n1 = LA(p, q->i1, q->j1, q->k1);
     6	
     7		h[n0] = q->f + q->g * (h[n1] - h[n0]);
     8		q->f = h[n1];
     9	}
    10	
    11	__global__
    12	static void murh_gpu(int64_t num, float *h, mur2_t *fmur)
    13	{
    14		int64_t n = threadIdx.x + (blockIdx.x * blockDim.x);
    15		if (n < num) {
    16			murh(h, &fmur[n], &d_Param);
    17		}
    18	}
    19	
    20	static void murh_cpu(int64_t num, float *h, mur2_t *fmur)
    21	{
    22		for (int64_t n = 0; n < num; n++) {
    23			murh(h, &fmur[n], &h_Param);
    24		}
    25	}
    26	
    27	void murHx_gpu()
    28	{
    29		assert(numMurHx > 0);
    30		if (GPU) {
    31			cudaMemcpyToSymbol(d_Param, &h_Param, sizeof(param_t));
    32			int grid = (numMurHx + (murBlock - 1)) / murBlock;
    33			murh_gpu<<<grid, murBlock>>>(numMurHx, Hx, fMurHx);
    34			cudaDeviceSynchronize();
    35		}
    36		else {
    37			murh_cpu(numMurHx, Hx, fMurHx);
    38		}
    39	}
    40	
    41	void murHy_gpu()
    42	{
    43		assert(numMurHy > 0);
    44		if (GPU) {
    45			cudaMemcpyToSymbol(d_Param, &h_Param, sizeof(param_t));
    46			int grid = (numMurHy + (murBlock - 1)) / murBlock;
    47			murh_gpu<<<grid, murBlock>>>(numMurHy, Hy, fMurHy);
    48			cudaDeviceSynchronize();
    49		}
    50		else {
    51			murh_cpu(numMurHy, Hy, fMurHy);
    52		}
    53	}
    54	
    55	void murHz_gpu()
    56	{
    57		assert(numMurHz > 0);
    58		if (GPU) {
    59			cudaMemcpyToSymbol(d_Param, &h_Param, sizeof(param_t));
    60			int grid = (numMurHz + (murBlock - 1)) / murBlock;
    61			murh_gpu<<<grid, murBlock>>>(numMurHz, Hz, fMurHz);
    62			cudaDeviceSynchronize();
    63		}
    64		else {
    65			murh_cpu(numMurHz, Hz, fMurHz);
    66		}
    67	}

4.5.4 PML吸収境界条件

PML吸収境界条件の内、例えばEx成分についてはリスト4-5-3のようになります。
62行目のように1次元配列を1次元のblockとgridに分割しています。


リスト4-5-3 PML吸収境界条件(Ex成分, pmlEx_gpu.cu)
     1	__host__ __device__
     2	static void pmlex(
     3		int i, int j, int k,
     4		float *ex, float *hy, float *hz, float *exy, float *exz,
     5		float rm1, float rm2, float gyn, float gzn, param_t *p)
     6	{
     7		float dhz = hz[LA(p, i, j, k)] - hz[LA(p, i, j - 1, k)];
     8		*exy = (*exy + (rm1 * dhz)) * gyn;
     9	
    10		float dhy = hy[LA(p, i, j, k)] - hy[LA(p, i, j, k - 1)];
    11		*exz = (*exz - (rm2 * dhy)) * gzn;
    12	
    13		ex[LA(p, i, j, k)] = *exy + *exz;
    14	}
    15	
    16	__global__
    17	static void pmlex_gpu(
    18		int ny, int nz,
    19		float *ex, float *hy, float *hz, float *exy, float *exz,
    20		int64_t numpmlex, pml2_t *fpmlex, float *rpmle, float *ryn, float *rzn,
    21		float *gpmlyn, float *gpmlzn, int l)
    22	{
    23		int n64_t = threadIdx.x + (blockIdx.x * blockDim.x);
    24		if (n < numpmlex) {
    25			int i = fpmlex[n].i;
    26			int j = fpmlex[n].j;
    27			int k = fpmlex[n].k;
    28			int m = fpmlex[n].m;
    29			float rm1 = (m != PEC) ? ryn[MIN(MAX(j, 0), ny    )] * rpmle[m] : 0;
    30			float rm2 = (m != PEC) ? rzn[MIN(MAX(k, 0), nz    )] * rpmle[m] : 0;
    31			pmlex(
    32				i, j, k,
    33				ex, hy, hz, &exy[n], &exz[n],
    34				rm1, rm2, gpmlyn[j + l], gpmlzn[k + l], &d_Param);
    35		}
    36	}
    37	
    38	static void pmlex_cpu(
    39		int ny, int nz,
    40		float *ex, float *hy, float *hz, float *exy, float *exz,
    41		int64_t numpmlex, pml2_t *fpmlex, float *rpmle, float *ryn, float *rzn,
    42		float *gpmlyn, float *gpmlzn, int l)
    43	{
    44		for (int64_t n = 0; n < numpmlex; n++) {
    45			int i = fpmlex[n].i;
    46			int j = fpmlex[n].j;
    47			int k = fpmlex[n].k;
    48			int m = fpmlex[n].m;
    49			float rm1 = (m != PEC) ? ryn[MIN(MAX(j, 0), ny    )] * rpmle[m] : 0;
    50			float rm2 = (m != PEC) ? rzn[MIN(MAX(k, 0), nz    )] * rpmle[m] : 0;
    51			pmlex(
    52				i, j, k,
    53				ex, hy, hz, &exy[n], &exz[n],
    54				rm1, rm2, gpmlyn[j + l], gpmlzn[k + l], &h_Param);
    55		}
    56	}
    57	
    58	void pmlEx_gpu()
    59	{
    60		if (GPU) {
    61			cudaMemcpyToSymbol(d_Param, &h_Param, sizeof(param_t));
    62			int grid = (numPmlEx + (pmlBlock - 1)) / pmlBlock;
    63			pmlex_gpu<<<grid, pmlBlock>>>(
    64				Ny, Nz,
    65				Ex, Hy, Hz, Exy, Exz,
    66				numPmlEx, fPmlEx, rPmlE, RYn, RZn,
    67				gPmlYn, gPmlZn, cPML.l);
    68			cudaDeviceSynchronize();
    69		}
    70		else {
    71			pmlex_cpu(
    72				Ny, Nz,
    73				Ex, Hy, Hz, Exy, Exz,
    74				numPmlEx, fPmlEx, rPmlE, RYn, RZn,
    75				gPmlYn, gPmlZn, cPML.l);
    76		}
    77	}

4.5.5 平均電磁界

リスト4-5-4に平均電磁界を計算する関数を示します。

ソースコードの説明
2行目:関数"average_e"は指定したセルの中心の電界3成分の絶対値の和を返します。
23行目:関数"average_h"は指定したセルの中心の磁界3成分の絶対値の和を返します。
46-48行目:blockを(Z,Y)方向の2次元、gridを(Z,Y,X)方向の3次元としています。
50行目:blockの大きさ(=スレッド数)を求めます。
51行目:スレッド番号を求めます。
52行目:ブロック番号を求めます。
60,68行目:スレッドの和(全体から見たら部分和)をreductionで求めます。
94-96行目:shared memoryは全スレッドで共有されるメモリーです。 reductionで必要になります。 shared memoryの大きさをexecution configulationの第3引数で渡します。 このときGPU側では44行目のように"extern __shared__"を付けます。
100-106行目:CPUで全体の和(=部分和の和)を計算します。


リスト4-5-4 平均電磁界(average)
     1	__host__ __device__
     2	static float average_e(float *ex, float *ey, float *ez, int i, int j, int k, param_t *p)
     3	{
     4		return
     5			fabsf(
     6				+ ex[LA(p, i,     j,     k    )]
     7				+ ex[LA(p, i,     j + 1, k    )]
     8				+ ex[LA(p, i,     j,     k + 1)]
     9				+ ex[LA(p, i,     j + 1, k + 1)]) +
    10			fabsf(
    11				+ ey[LA(p, i,     j,     k    )]
    12				+ ey[LA(p, i,     j,     k + 1)]
    13				+ ey[LA(p, i + 1, j,     k    )]
    14				+ ey[LA(p, i + 1, j,     k + 1)]) +
    15			fabsf(
    16				+ ez[LA(p, i,     j,     k    )]
    17				+ ez[LA(p, i + 1, j,     k    )]
    18				+ ez[LA(p, i,     j + 1, k    )]
    19				+ ez[LA(p, i + 1, j + 1, k    )]);
    20	}
    21	
    22	__host__ __device__
    23	static float average_h(float *hx, float *hy, float *hz, int i, int j, int k, param_t *p)
    24	{
    25		return
    26			fabsf(
    27				+ hx[LA(p, i,     j,     k    )]
    28				+ hx[LA(p, i + 1, j,     k    )]) +
    29			fabsf(
    30				+ hy[LA(p, i,     j,     k    )]
    31				+ hy[LA(p, i,     j + 1, k    )]) +
    32			fabsf(
    33				+ hz[LA(p, i,     j,     k    )]
    34				+ hz[LA(p, i,     j,     k + 1)]);
    35	}
    36	
    37	// GPU
    38	__global__
    39	static void average_gpu(
    40		int nx, int ny, int nz,
    41		float *ex, float *ey, float *ez, float *hx, float *hy, float *hz,
    42		float *sume, float *sumh)
    43	{
    44		extern __shared__ float sm[];
    45	
    46		int i = blockIdx.z;
    47		int j = threadIdx.y + (blockIdx.y * blockDim.y);
    48		int k = threadIdx.x + (blockIdx.x * blockDim.x);
    49	
    50		int nthread = blockDim.x * blockDim.y;
    51		int tid = threadIdx.x + (threadIdx.y * blockDim.x);
    52		int bid = blockIdx.x + (blockIdx.y * gridDim.x) + (blockIdx.z * gridDim.x * gridDim.y);
    53	
    54		if ((i < nx) && (j < ny) && (k < nz)) {
    55			sm[tid] = average_e(ex, ey, ez, i, j, k, &d_Param);
    56		}
    57		else {
    58			sm[tid] = 0;
    59		}
    60		reduction_sum(tid, nthread, sm, &sume[bid]);
    61	
    62		if ((i < nx) && (j < ny) && (k < nz)) {
    63			sm[tid] = average_h(hx, hy, hz, i, j, k, &d_Param);
    64		}
    65		else {
    66			sm[tid] = 0;
    67		}
    68		reduction_sum(tid, nthread, sm, &sumh[bid]);
    69	}
    70	
    71	// CPU
    72	static void average_cpu(float *sum)
    73	{
    74		sum[0] = 0;
    75		sum[1] = 0;
    76		for (int i = 0; i < Nx; i++) {
    77		for (int j = 0; j < Ny; j++) {
    78		for (int k = 0; k < Nz; k++) {
    79			sum[0] += average_e(Ex, Ey, Ez, i, j, k, &h_Param);
    80			sum[1] += average_h(Hx, Hy, Hz, i, j, k, &h_Param);
    81		}
    82		}
    83		}
    84	}
    85	
    86	void average(double fsum[])
    87	{
    88		float sum[2];
    89	
    90		// sum
    91		if (GPU) {
    92			cudaMemcpyToSymbol(d_Param, &h_Param, sizeof(param_t));
    93	
    94			int sm_size = sumBlock.x * sumBlock.y * sizeof(float);
    95			average_gpu<<<sumGrid, sumBlock, sm_size>>>(
    96				Nx, Ny, Nz, Ex, Ey, Ez, Hx, Hy, Hz, m_sumE, m_sumH);
    97			cudaDeviceSynchronize();
    98	
    99			// partial sum
   100			sum[0] = 0;
   101			sum[1] = 0;
   102			int n = sumGrid.x * sumGrid.y * sumGrid.z;
   103			for (int i = 0; i < n; i++) {
   104				sum[0] += m_sumE[i];
   105				sum[1] += m_sumH[i];
   106			}
   107		}
   108		else {
   109			average_cpu(sum);
   110		}
   111	
   112		// average
   113		fsum[0] = sum[0] / (4.0 * Nx * Ny * Nz);
   114		fsum[1] = sum[1] / (2.0 * Nx * Ny * Nz);
   115	}

リスト4-5-5にreduction関数を示します。
reductionとはN個の演算を複数のスレッドの共同作業でlog2N回で求めるGPUの操作です。
引数"s"がshared memoryでありスレッド間で共有されます。
図4-5-1にreductionの概念図を示します。
詳細についてはCUDAのサンプルプログラム"reduction"を参考にして下さい。


リスト4-5-5 reduction関数
     1	__device__
     2	void reduction_sum(int tid, int n, float *s, float *sum)
     3	{
     4		__syncthreads();
     5	
     6		for (int stride = (n + 1) >> 1; n > 1; stride = (stride + 1) >> 1, n = (n + 1) >> 1) {
     7			if (tid + stride < n) {
     8				s[tid] += s[tid + stride];
     9			}
    10			__syncthreads();
    11		}
    12	
    13		if (tid == 0) {
    14			*sum = s[0];
    15		}
    16	}


図4-5-1 reductionの概念図

4.5.6 CUDAの計算時間

表4-5-1にWindowsでの計算時間の測定結果を示します。
参考までに表4-2-1のCPU1コア(SIMDなし)の結果も載せます。
GPUを用いるとCPU1コアの8-11倍速くなることがわかります。 一般に問題のサイズが大きくなるほど速度比も上がります。

表4-5-1 CUDAの計算時間(Windows, ()内はCPU1コアとの速度比)
ベンチマークNo.CPU(1コア)GPU
127.6秒(1.0)3.3秒(8.4)
2229.6秒(1.0)21.4秒(10.7)

4.5.7 ブロックの大きさ

リスト4-5-1の60-62行目のブロックの大きさによって計算時間が変わり、 ブロックの大きさには最適値が存在します。
図4-5-2にblockとgridの概念図を示します。
表4-5-2にblockDim.xとblockDim.yを変えたときの計算時間を示します。
色のついた組み合わせのとき計算時間が最小になります。 すなわちblockDim.xは32以上、 ブロックサイズ(= blockDim.x * blockDim.y)が128か256が最適値となります。 この最適値は問題のサイズにはあまり関係しません。 なお、ブロックサイズの上限はCompute Capability 2.0以降では1024です。
以上の結果から本プログラムでは blockDim.x = 32, blockDim.y = 8 とします。 前者が最も小さい組み合わせを選んだ理由は、Z方向のセル数Nzが常に十分大きいとは限らないので、 Nzが小さいときはblockDim.xが小さい方が効率よく計算できるためです。


図4-5-2 FDTD法におけるblockとgridの概念図

表4-5-2 CUDAの計算時間とblockの関係(Windows、ベンチマークNo.2、blockDim.z=1)
blockDim.yblockDim.x
163264128256
176.8秒43.9秒28.5秒22.3秒22.4秒
244.2秒26.0秒22.4秒22.9秒25.3秒
428.6秒22.2秒22.9秒25.6秒29.5秒
829.0秒22.5秒25.8秒31.8秒N/A
1629.0秒24.1秒31.6秒N/AN/A