第15回 CPUとGPUの協調
長岡技術科学大学 電気電子情報工学専攻 出川智啓
今回の内容
2015/07/23先端GPGPUシミュレーション工学特論2
 並列処理と並行処理
 ストリーム
 複数ストリームを用いたベクトル和の計算
 CPUとGPUを用いたベクトル和の計算
並列処理と並行処理
2015/07/23先端GPGPUシミュレーション工学特論3
 並列処理(Parallel Processing)
 一つの処理を複数の処理に分割
 協調しながら複数の処理を実行
 プログラムを高速化するために行われる
 並行処理(Concurrent Processing)
 複数の処理を実行
 複数の処理は必ずしも協調していない
 利便性の向上のために行われる
 OSが複数のプログラムを実行する等
CUDAプログラムの実行
 実行時の流れ(CPU視点)
 利用するGPUの初期化やデータの転送などを実行
 GPUで実行する関数を呼び出し
 GPUから結果を取得
初期化の指示
初期化
カーネルの実行指示
カーネルを実行
結果の取得
実行結果をコピー
time
CPUとGPUは非同期
CPUは別の処理を実行可能
2015/07/23先端GPGPUシミュレーション工学特論4
必要なデータのコピー
メモリに書込
GPUとCPUの並行実行
2015/07/23先端GPGPUシミュレーション工学特論5
 GPUのみの処理
float *a = (float *)malloc(NBytes);
float *dev_a;
cudaMalloc((void **)&dev_a, NBytes);
cudaMemcpy(dev_a, a, NBytes, cudaMemcpyHostToDevice);
kernel<<<NB, NT>>>(dev_a);
cudaMemcpy(a, dev_a, NBytes, cudaMemcpyDeviceToHost);
aを転送(H2D) kernel dev_aを転送(D2H)GPU
待機 待機CPU
cudaMemcpy実行 カーネル起動 cudaMemcpy実行
GPUとは同期せず(カーネルの終了を待たず),
直ちに次の処理を実行
GPUとCPUの並行実行
2015/07/23先端GPGPUシミュレーション工学特論6
 GPUとCPUの非同期処理
float *a = (float *)malloc(NBytes), *dev_a;
cudaMalloc((void **)&dev_a, NBytes);
cudaMemcpy(dev_a, a, NBytes, cudaMemcpyHostToDevice);
kernel<<<NB, NT>>>(dev_a); 
do_something(a);
cudaMemcpy(a, dev_a, NBytes, cudaMemcpyDeviceToHost);
CPUとGPUは非同期で処理を実行
aを転送(H2D) kernel 待機 dev_aを転送(D2H)GPU
待機 do_something() 待機CPU
cudaMemcpy実行 カーネル起動 cudaMemcpy実行
GPUとCPUの並行実行
2015/07/23先端GPGPUシミュレーション工学特論7
 GPUとCPUが非同期で行う処理とは
 cudaMemcpy
 同期通信(synchronous)
 同期が完了するまでCPUとGPUが他の処理を実行しない
 ブロッキング(blocking)型とも呼ばれる
 カーネル実行
 非同期で実行(asynchronous)
 カーネルを実行した直後にCPUが他の処理を実行可能
GPUの並行実行(要望)
2015/07/23先端GPGPUシミュレーション工学特論8
 通信(データ転送)を同時に行いたい
 通信(データ転送)している間に計算を実行したい
 負荷の軽い計算を複数同時に実行したい
float *a, *b, *dev_a, *dev_b;
a = (float *)malloc(NBytes);
b = (float *)malloc(NBytes);
cudaMalloc((void **)&dev_a, NBytes);
cudaMalloc((void **)&dev_b, NBytes);
cudaMemcpy(dev_a, a, NBytes, cudaMemcpyHostToDevice);
cudaMemcpy(dev_b, b, NBytes, cudaMemcpyHostToDevice);
kernel1<<<1, NT>>>(dev_a);
kernel2<<<1, NT>>>(dev_b);
cudaMemcpy(a, dev_a, NBytes, cudaMemcpyDeviceToHost);
cudaMemcpy(b, dev_b, NBytes, cudaMemcpyDeviceToHost);
転送が終わると直ち
にカーネル実行
負荷が軽いカーネル
は同時に実行
カーネルが終わると
直ちに転送
GPUの並行実行(要望)
2015/07/23先端GPGPUシミュレーション工学特論9
cudaMemcpy(dev_a, a, NBytes, cudaMemcpyHostToDevice);
cudaMemcpy(dev_b, b, NBytes, cudaMemcpyHostToDevice);
kernel1<<<1, NT>>>(dev_a);
kernel2<<<1, NT>>>(dev_b);
cudaMemcpy(a, dev_a, NBytes, cudaMemcpyDeviceToHost);
cudaMemcpy(b, dev_b, NBytes, cudaMemcpyDeviceToHost);
転送が終わると直ち
にカーネル実行
負荷が軽いカーネル
は同時に実行
カーネルが終わると
直ちに転送
aを転送(H2D) kernel1 dev_aを転送(D2H)
bを転送(H2D) kernel2 dev_bを転送(D2H)
転送の
並行実行
転送と
カーネ
ルの並
行実行
カーネルの
並行実行
転送とカーネル
の並行実行
転送の
並行実行
GPUの並行実行(要望)
2015/07/23先端GPGPUシミュレーション工学特論10
cudaMemcpy(dev_a, a, NBytes, cudaMemcpyHostToDevice);
cudaMemcpy(dev_b, b, NBytes, cudaMemcpyHostToDevice);
kernel1<<<1, NT>>>(dev_a);
kernel2<<<1, NT>>>(dev_b);
cudaMemcpy(a, dev_a, NBytes, cudaMemcpyDeviceToHost);
cudaMemcpy(b, dev_b, NBytes, cudaMemcpyDeviceToHost);
転送が終わると直ち
にカーネル実行
負荷が軽いカーネル
は同時に実行
カーネルが終わると
直ちに転送
aを転送(H2D) kernel1 dev_aを転送(D2H)
bを転送(H2D) kernel2 dev_bを転送(D2H)
転送の
並行実行
転送と
カーネ
ルの並
行実行
カーネルの
並行実行
転送とカーネル
の並行実行
転送の
並行実行
転送の方向が異
なる場合は可能
転送の方向が異
なる場合は可能
Concurrent Kernel Execution
2015/07/23先端GPGPUシミュレーション工学特論11
 GPUが並行に処理を実行可能かの確認
 cudaGetDeviceProperties()のメンバdeviceOverlap
を確認
 1なら並行実行可能
int dev = 0;
cudaDeviceProp deviceProp;
cudaGetDeviceProperties(&deviceProp, dev);
if(deviceProp.deviceOverlap == 1)
printf("Device %d: ¥"%s¥" supports concurrent kernel execution¥n",
dev, deviceProp.name);
concurrent.cu
GPUの並行実行
2015/07/23先端GPGPUシミュレーション工学特論12
 データ通信とカーネル実行を並行に実行可能
 データ通信と並行に実行できるカーネルは一つ
 CPUからGPUへの転送,GPUからCPUへの転送,カーネル実
行を並行に実行可能
 Compute Capability 2.0以上のGPU
 pgaccelinfoのDevice Revision Numberと同じ
 複数のカーネルを並行に実行
 並行実行可能なカーネルの数
 32 Compute Capability 3.5以上
 16 Compute Capability 2.0以上3.5未満
 並行実行するカーネルが資源を利用できることが条件
ストリーム
2015/07/23先端GPGPUシミュレーション工学特論13
 GPUで実行される処理の流れ
 同じストリームに属する処理は,必ず命令発行され
た順に実行される
 異なるストリームに属する処理は並行に実行できる
 異なるストリームに属する処理間に依存性がないと仮定
 ストリームを複数作ることで処理を並行に実行
標準のストリーム(ストリーム0)
2015/07/23先端GPGPUシミュレーション工学特論14
 ストリームを指定しない場合はストリーム0に所属
 ストリーム0は他の処理とは同時に実行されない
 ストリーム0はCPUと同期して実行
 例外はカーネル実行,明示的な非同期通信等
 並行に実行するためには,0でない複数のストリーム
に所属させる必要がある
float *a, *b, *dev_a, *dev_b;
cudaStream_t stream1, stream2;
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);
cudaMalloc((void **)&dev_a, NBytes);
cudaMalloc((void **)&dev_b, NBytes);
cudaHostAlloc((void **)&a, NBytes, cudaHostAllocDefault);
cudaHostAlloc((void **)&b, NBytes, cudaHostAllocDefault);
cudaMemcpyAsync(dev_a, a, Nbytes, cudaMemcpyHostToDevice, stream1);
cudaMemcpyAsync(dev_b, b, Nbytes, cudaMemcpyHostToDevice, stream2);
kernel1<<<1, NT, 0, stream1>>>(dev_a);
kernel2<<<1, NT, 0, stream2>>>(dev_b);
cudaMemcpyAsync(a, dev_a, Nbytes, cudaMemcpyDeviceToHost, stream1);
cudaMemcpyAsync(a, dev_a, Nbytes, cudaMemcpyDeviceToHost, stream2);
cudaDeviceSynchronize();
ストリームを用いた並行実行
2015/07/23先端GPGPUシミュレーション工学特論15
ストリームを用いた並行実行
2015/07/23先端GPGPUシミュレーション工学特論16
 ストリームの作成(具体的に番号を付けるわけではない)
 cudaStreamCreate(cudaStream_t *);
 ストリームの破棄
 cudaStreamDestroy(cudaStream_t);
cudaStream_t stream1, stream2,stream[10];
cudaStreamCreate(&stream1); //個別の変数でストリームを管理することが可能
cudaStreamCreate(&stream2); //
for(int i=0;i<10;i++)
cudaStreamCreate(&stream[i]);//配列でストリームを管理することも可能
cudaStreamDestroy(stream1); 
cudaStreamDestroy(stream2); 
for(int i=0;i<10;i++)
cudaStreamDestroy(stream[i]);
ストリームを用いた並行実行
2015/07/23先端GPGPUシミュレーション工学特論17
 cudaMemcpyAsync
 非同期転送(asynchronous)
 5番目の引数としてストリームを指定
 CPUのメモリには必ずページロックメモリを指定
 cudaHostAlloc()もしくはcudaMallocHost()で確保
 カーネル実行
 <<<>>>内の4番目にストリームを指定
 (1)ブロック数 (2)1ブロックあたりのスレッド数
(3)共有メモリサイズ (4)ストリーム
 共有メモリのサイズを指定しない場合は0を記述
並行実行の同期
2015/07/23先端GPGPUシミュレーション工学特論18
 cudaDeviceSynchronize()
 GPUで実行している全ての処理が完了するまで待機
 cudaStreamSynchronize(ストリーム)
 引数で指定したストリームに属する命令が完了するまで待機
…
cudaMemcpyAsync(a, dev_a, Nbytes, cudaMemcpyDeviceToHost, stream1);
cudaMemcpyAsync(b, dev_b, Nbytes, cudaMemcpyDeviceToHost, stream2);
cudaDeviceSynchronize(); //dev_a,dev_b両方の転送が終わるまで待機
…
cudaMemcpyAsync(a, dev_a, Nbytes, cudaMemcpyDeviceToHost, stream1);
cudaMemcpyAsync(b, dev_b, Nbytes, cudaMemcpyDeviceToHost, stream2);
cudaStreamSynchronize(stream1); //dev_aの転送が終わると次の処理へ移行
ベクトル和C=A+Bの並行実行
2015/07/23先端GPGPUシミュレーション工学特論19
 並行実行の方針
 複数のストリームを作成
 配列のまとまったサイズを異なるストリームに所属
 A, Bの初期化はCPUで実行し,GPUへ転送
 A, Bの転送が終わったストリームから足し算を実行し,
CPUへCを転送
ベクトル和C=A+Bの並行実行
2015/07/23先端GPGPUシミュレーション工学特論20
 同期実行
 並行実行
 処理をオーバーラップできれば若干高速化
Aを転送(H2D) Bを転送(H2D) add dev_Cを転送(D2H)
A(H2D) B(H2D) add dev_C(D2H)
A(H2D) B(H2D) add dev_C(D2H)
A(H2D) B(H2D) add dev_C(D2H)
Stream 0
Stream 1
Stream 2
Stream 3 高速化
実行開始 処理時間
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>
#define N (1024*1024*8)
#define Nbytes (N*sizeof(float))
#define NT 256
#define NB (N/NT)
//Streamの数=並行実行数
#define Stream 4
//カーネルは変更なし
__global__ void init
(float *a, float *b, float *c){
int i = blockIdx.x*blockDim.x
+ threadIdx.x;
a[i] = 1.0;
b[i] = 2.0;
c[i] = 0.0;
}
//カーネルは変更なし
__global__ void add
(float *a, float *b, float *c){
int i = blockIdx.x*blockDim.x
+ threadIdx.x;
c[i] = a[i] + b[i];
}
ベクトル和(ストリームの利用)
2015/07/23先端GPGPUシミュレーション工学特論21
vectoradd_stream.cu
int main(){
float *a,*b,*c;
int stm;
cudaStream_t stream[Stream];
//ページロックホストメモリを確保
float *host_a, *host_b, *host_c;
cudaHostAlloc((void **)&host_c,
Nbytes, cudaHostAllocDefault);
cudaHostAlloc((void **)&host_a,
Nbytes, cudaHostAllocDefault);
cudaHostAlloc((void **)&host_b,
Nbytes, cudaHostAllocDefault);
for(int i=0;i<N;i++){
host_a[i] = 1.0f;
host_b[i] = 2.0f;
host_c[i] = 0;
}
for(stm=0;stm<Stream;stm++){
cudaStreamCreate(&stream[stm]);
}
cudaMalloc( (void **)&a, Nbytes);
cudaMalloc( (void **)&b, Nbytes);
cudaMalloc( (void **)&c, Nbytes);
ベクトル和(ストリームの利用)
2015/07/23先端GPGPUシミュレーション工学特論22
vectoradd_stream.cu
double time_start = omp_get_wtime();
for(stm=0;stm<Stream;stm++){
int idx = stm*N/Stream;
cudaMemcpyAsync(&a[idx], &host_a[idx], Nbytes/Stream, cudaMemcpyHostToDevice, 
stream[stm]);
cudaMemcpyAsync(&b[idx], &host_b[idx], Nbytes/Stream, cudaMemcpyHostToDevice, 
stream[stm]);
add<<< NB/Stream, NT, 0 ,stream[stm]>>>(&a[idx],&b[idx],&c[idx]);
cudaMemcpyAsync(&host_c[idx], &c[idx], Nbytes/Stream, cudaMemcpyDeviceToHost, 
stream[stm]);
}
cudaDeviceSynchronize();
double time_end = omp_get_wtime();
ベクトル和(ストリームの利用)
2015/07/23先端GPGPUシミュレーション工学特論23
vectoradd_stream.cu
double sum=0;
for(int i=0;i<N;i++)sum+=host_c[i];
printf("%f¥n",sum/N);
printf("elapsed time = %f sec¥n",
time_end‐time_start);
cudaFreeHost(host_a);
cudaFreeHost(host_b);
cudaFreeHost(host_c);
cudaFree(a);
cudaFree(b);
cudaFree(c);
for(stm=0;stm<Stream;stm++){
cudaStreamDestroy(stream[stm]);
}
return 0;
}
ベクトル和(ストリームの利用)
2015/07/23先端GPGPUシミュレーション工学特論24
vectoradd_stream.cu
実行結果
2015/07/23先端GPGPUシミュレーション工学特論25
 配列の要素数 N=223
 1ブロックあたりのスレッド数 256
 OpenMPの関数を用いて実行時間を測定
 コンパイルにはオプション‐Xcompiler ‐fopenmpが必要
ストリーム数 実行時間[ms]
1 17.5
2 15.5
4 14.5
8 14.1
16 14.0
ストリーム数
実行時間[ms]
プロファイラによる確認
2015/07/23先端GPGPUシミュレーション工学特論26
 ストリーム数4
 メモリ転送とベクトル和を4セット実行
# CUDA_PROFILE_LOG_VERSION 2.0
# CUDA_DEVICE 0 Tesla M2050
# TIMESTAMPFACTOR fffff60a9ac44950
timestamp,method,gputime,cputime,occupancy
timestamp=[ 92925.000 ] method=[ memcpyHtoDasync ] gputime=[ 1403.488 ] cputime=[ 16.000 ]
timestamp=[ 92948.000 ] method=[ memcpyHtoDasync ] gputime=[ 1410.880 ] cputime=[ 5.000 ]
timestamp=[ 92977.000 ] method=[ _Z3addPfS_S_ ] gputime=[ 253.472 ] cputime=[ 23.000 ] occupancy=[ 1.000 ]
timestamp=[ 93005.000 ] method=[ memcpyDtoHasync ] gputime=[ 1520.448 ] cputime=[ 6.000 ]
timestamp=[ 93014.000 ] method=[ memcpyHtoDasync ] gputime=[ 1931.936 ] cputime=[ 6.000 ]
timestamp=[ 93021.000 ] method=[ memcpyHtoDasync ] gputime=[ 1414.272 ] cputime=[ 5.000 ]
timestamp=[ 93029.000 ] method=[ _Z3addPfS_S_ ] gputime=[ 253.248 ] cputime=[ 8.000 ] occupancy=[ 1.000 ]
timestamp=[ 93038.000 ] method=[ memcpyDtoHasync ] gputime=[ 1521.216 ] cputime=[ 5.000 ]
timestamp=[ 93045.000 ] method=[ memcpyHtoDasync ] gputime=[ 1935.456 ] cputime=[ 6.000 ]
timestamp=[ 93053.000 ] method=[ memcpyHtoDasync ] gputime=[ 1417.152 ] cputime=[ 6.000 ]
timestamp=[ 93060.000 ] method=[ _Z3addPfS_S_ ] gputime=[ 252.352 ] cputime=[ 7.000 ] occupancy=[ 1.000 ]
timestamp=[ 93068.000 ] method=[ memcpyDtoHasync ] gputime=[ 1566.624 ] cputime=[ 6.000 ]
timestamp=[ 93076.000 ] method=[ memcpyHtoDasync ] gputime=[ 1978.208 ] cputime=[ 6.000 ]
timestamp=[ 93083.000 ] method=[ memcpyHtoDasync ] gputime=[ 1414.528 ] cputime=[ 6.000 ]
timestamp=[ 93090.000 ] method=[ _Z3addPfS_S_ ] gputime=[ 225.568 ] cputime=[ 8.000 ] occupancy=[ 1.000 ]
timestamp=[ 93099.000 ] method=[ memcpyDtoHasync ] gputime=[ 1319.040 ] cputime=[ 6.000 ]
Nsightによる確認
2015/07/23先端GPGPUシミュレーション工学特論27
 ストリーム数8
 カーネル実行と転送(CPUからGPU,GPUからCPU)がオー
バーラップ
CPUとGPUの協調
2015/07/23先端GPGPUシミュレーション工学特論28
 cudaDeviceSynchronize()が呼ばれるまでCPUと
GPUは非同期
 GPUでベクトル和を計算している間,CPUは待機
 処理の一部をCPUが実行する事で高速化が可能
 GPUのカーネル,非同期転送処理を全て呼び終わった後に
CPUでもベクトル和を計算
A(H2D) B(H2D) add dev_C(D2H)
A(H2D) B(H2D) add dev_C(D2H)
転送・カー
ネル呼出 add
Stream 1
Stream 2
CPU
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>
#define N (1024*1024*8)
#define Nbytes (N*sizeof(float))
#define NT 256
#define NB (N/NT)
//Streamの数=並行実行数
#define Stream 4
//CPUのStream分担数
//GPUはStream‐CPU分の数だけ並行処理を実行
#define CPU 1
//カーネルは変更なし
__global__ void init
(float *a, float *b, float *c){
int i = blockIdx.x*blockDim.x
+ threadIdx.x;
a[i] = 1.0;
b[i] = 2.0;
c[i] = 0.0;
}
//カーネルは変更なし
__global__ void add
(float *a, float *b, float *c){
int i = blockIdx.x*blockDim.x
+ threadIdx.x;
c[i] = a[i] + b[i];
}
ベクトル和(CPU・GPU協調)
2015/07/23先端GPGPUシミュレーション工学特論29
vectoradd_coop.cu
int main(){
float *a,*b,*c;
int stm;
cudaStream_t stream[Stream];
float *host_a, *host_b, *host_c;
cudaHostAlloc((void **)&host_c,
Nbytes, cudaHostAllocDefault);
cudaHostAlloc((void **)&host_a,
Nbytes, cudaHostAllocDefault);
cudaHostAlloc((void **)&host_b,
Nbytes, cudaHostAllocDefault);
for(int i=0;i<N;i++){
host_a[i] = 1.0f;
host_b[i] = 2.0f;
host_c[i] = 0;
}
for(stm=0;stm<Stream‐CPU;stm++){
cudaStreamCreate(&stream[stm]);
}
cudaMalloc( (void **)&a, Nbytes);
cudaMalloc( (void **)&b, Nbytes);
cudaMalloc( (void **)&c, Nbytes);
ベクトル和(CPU・GPU協調)
2015/07/23先端GPGPUシミュレーション工学特論30
vectoradd_coop.cu
double time_start = omp_get_wtime();
for(stm=0;stm<Stream‐CPU;stm++){
int idx = stm*N/Stream;
cudaMemcpyAsync(&a[idx], &host_a[idx], Nbytes/Stream, cudaMemcpyHostToDevice, 
stream[stm]);
cudaMemcpyAsync(&b[idx], &host_b[idx], Nbytes/Stream, cudaMemcpyHostToDevice, 
stream[stm]);
add<<< NB/Stream, NT, 0 ,stream[stm]>>>(&a[idx],&b[idx],&c[idx]);
cudaMemcpyAsync(&host_c[idx], &c[idx], Nbytes/Stream, cudaMemcpyDeviceToHost, 
stream[stm]);
}
for(int i=(Stream‐CPU)*N/Stream;i<N;i++)
host_c[i] = host_a[i] + host_b[i];
cudaDeviceSynchronize();
double time_end = omp_get_wtime();
ベクトル和(CPU・GPU協調)
2015/07/23先端GPGPUシミュレーション工学特論31
vectoradd_coop.cu
double sum=0;
for(int i=0;i<N;i++)sum+=host_c[i];
printf("%f¥n",sum/N);
printf("elapsed time = %f sec¥n",
time_end‐time_start);
cudaFreeHost(host_a);
cudaFreeHost(host_b);
cudaFreeHost(host_c);
cudaFree(a);
cudaFree(b);
cudaFree(c);
for(stm=0;stm<Stream‐CPU;stm++){
cudaStreamDestroy(stream[stm]);
}
return 0;
}
ベクトル和(CPU・GPU協調)
2015/07/23先端GPGPUシミュレーション工学特論32
vectoradd_coop.cu
実行結果
2015/07/23先端GPGPUシミュレーション工学特論33
 配列の要素数 N=223
 1ブロックあたりのスレッド数 256
 ストリームの数,CPUの負荷割合を変えて計算
 CPU負荷割合=CPUが計算するストリーム数/ストリーム数
 0のとき全てGPUで計算,1のとき全てCPUで計算
 OpenMPの関数を用いて実行時間を測定
 コンパイルにはオプション‐Xcompiler ‐fopenmpが必要
実行結果
2015/07/23先端GPGPUシミュレーション工学特論34
ストリーム
数
CPU
負荷割合
実行時間
[ms]
1
0 (GPU) 17.6
1 (CPU) 36.4
2
0.0 15.5
0.5 18.3
1.0 36.4
4
0.00 14.5
0.25 11.2
0.50 18.3
0.75 27.4
1.00 36.3
ストリーム
数
CPU
負荷割合
実行時間
[ms]
8
0.000 14.1
0.125 12.5
0.250 10.8
0.375 13.8
0.500 18.3
0.625 22.9
0.750 27.4
0.875 31.9
1.000 36.3
実行結果
2015/07/23先端GPGPUシミュレーション工学特論35
ストリーム
数
CPU
負荷割合
実行時間
[ms]
16
0.0000 14.0
0.0625 13.1
0.1250 12.2
0.1875 11.5
0.2500 10.6
0.3125 11.6
0.3750 13.9
0.4375 16.2
0.5000 18.4
ストリーム
数
CPU
負荷割合
実行時間
[ms]
16
0.5625 20.6
0.6250 22.9
0.6875 25.1
0.7500 27.5
0.8125 29.6
0.8750 31.9
0.9375 34.3
1.0000 36.3
grouseではCPUが全体の処理の1/4を処理すると最も効率が良い
実行結果
2015/07/23先端GPGPUシミュレーション工学特論36
CPU負荷割合
実行時間[ms]
ストリーム数
OpenMPによる処理の効率化
2015/07/23先端GPGPUシミュレーション工学特論37
 CPUとGPUの協調版
 1スレッドが転送やカーネルを呼び出し
 全て呼び出した後にベクトル和を実行
 複数のスレッドを起動
 1スレッドをカーネル起動,非同期転送呼出に充てる
 CPUではベクトル和を並列に計算
OpenMP
2015/07/23先端GPGPUシミュレーション工学特論38
 並列に処理を実行させる箇所に指示句(ディレクティ
ブ)を挿入
 for文の並列化
 ディレクティブを一行追加(#pragma omp ~)
#pragma omp parallel for
for(int i=0; i<N; i++)
C[i] = A[i] + B[i]
#include<stdio.h>
#include<stdlib.h>
#define N (1024*1024)
#define Nbytes (N*sizeof(float))
int main(){
float *a,*b,*c;
int i;
a = (float *)malloc(Nbytes);
b = (float *)malloc(Nbytes);
c = (float *)malloc(Nbytes);
for(i=0; i<N; i++){
a[i] = 1.0;
b[i] = 2.0;
c[i] = 0.0;
}
for(i=0; i<N; i++)
c[i] = a[i] + b[i];
for(i=0; i<N; i++)
printf("%f+%f=%f¥n",
a[i],b[i],c[i]);
return 0;
}
逐次(並列化前)プログラム
2015/07/23先端GPGPUシミュレーション工学特論39
並列化プログラム
2015/07/23先端GPGPUシミュレーション工学特論40
#include<stdio.h>
#include<stdlib.h>
#define N (1024*1024)
#define Nbytes (N*sizeof(float))
int main(){
float *a,*b,*c;
int i;
a = (float *)malloc(Nbytes);
b = (float *)malloc(Nbytes);
c = (float *)malloc(Nbytes);
#pragma omp parallel
{
#pragma omp for
for(i=0; i<N; i++){
a[i] = 1.0;
b[i] = 2.0;
c[i] = 0.0;
}
#pragma omp for
for(i=0; i<N; i++)
c[i] = a[i] + b[i];
}
for(i=0; i<N; i++)
printf("%f+%f=%f¥n",
a[i],b[i],c[i]);
return 0;
}
OpenMPの指示文
2015/07/23先端GPGPUシミュレーション工学特論41
 並列処理制御
 OpenMPで並列処理を行う領域の定義
 並列実行領域(Parallel Region)構文
 ワークシェアリング(Work sharing)構文
 同期制御
 OpenMP並列領域内でのデータアクセス,命令実行の同期
 データ属性制御
 並列領域内で利用されるデータの属性を定義
 その他
並列実行領域(Parallel Region)構文
2015/07/23先端GPGPUシミュレーション工学特論42
 parallel構文
 parallel構文で指示された領域では指定されたスレッド
が並列に処理を実行
 全てのスレッドが同じ処理を実行
#pragma omp parallel //{ <‐ここに括弧を書くとエラー
{
複数のスレッドが起動され,ここに書いてある処理を実行
全てのスレッドが同じ処理を実行
}
ワークシェアリング(Work sharing)構文
2015/07/23先端GPGPUシミュレーション工学特論43
 for構文
 parallel構文で指定された並列実行領域内で利用
 直後のforループを各スレッドに分割して並列処理を実行
 for(初期化;継続条件;再初期化)で構成されるforルー
プが対象
 全てのスレッドが処理を終了するまで他のスレッドは待機
#pragma omp parallel
{
#pragma omp for
for(i=0; i<N; i++){
forループを自動的に分割して各スレッドが実行
}
全てのスレッドが処理を終了するまで待機
}
single構文
2015/07/23先端GPGPUシミュレーション工学特論44
 parallel構文で指定された並列実行領域内で利用
 一つのスレッドのみが処理を実行
 処理を終了するまで他のスレッドは待機
 待機させる必要がない場合はnowait節を指定
#pragma omp parallel
{
#pragma omp single
{
1スレッドのみが処理を実行
他のスレッドは待機
}
single構文内のスレッドが処理を終了するまで待機
}
nowait指示節
2015/07/23先端GPGPUシミュレーション工学特論45
 ワークシェア構文で指定されたブロックの最後で同
期せず,処理を継続
 全スレッドが処理を終了するまで待たず,次の処理を実行
#pragma omp parallel
{
#pragma omp single nowait
{
1スレッドのみが処理を実行
他のスレッドは待機せず,以降の処理を実行
}
...
}
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>
#define N (1024*1024*8)
#define Nbytes (N*sizeof(float))
#define NT 256
#define NB (N/NT)
#define Stream 4
#define CPU 2
#define Threads 12
//カーネルは変更なし
__global__ void init
(float *a, float *b, float *c){
int i = blockIdx.x*blockDim.x
+ threadIdx.x;
a[i] = 1.0;
b[i] = 2.0;
c[i] = 0.0;
}
//カーネルは変更なし
__global__ void add
(float *a, float *b, float *c){
int i = blockIdx.x*blockDim.x
+ threadIdx.x;
c[i] = a[i] + b[i];
}
ベクトル和(CPUをOpenMPで並列化)
2015/07/23先端GPGPUシミュレーション工学特論46
vectoradd_coop_omp.cu
int main(){
float *a,*b,*c;
int stm;
cudaStream_t stream[Stream];
float *host_a, *host_b, *host_c;
cudaHostAlloc((void **)&host_c,
Nbytes, cudaHostAllocDefault);
cudaHostAlloc((void **)&host_a,
Nbytes, cudaHostAllocDefault);
cudaHostAlloc((void **)&host_b,
Nbytes, cudaHostAllocDefault);
for(int i=0;i<N;i++){
host_a[i] = 1.0f;
host_b[i] = 2.0f;
host_c[i] = 0;
}
for(stm=0;stm<Stream‐CPU;stm++){
cudaStreamCreate(&stream[stm]);
}
cudaMalloc( (void **)&a, Nbytes);
cudaMalloc( (void **)&b, Nbytes);
cudaMalloc( (void **)&c, Nbytes);
omp_set_num_threads(Threads);
double time_start = omp_get_wtime();
#pragma omp parallel
{
ベクトル和(CPUをOpenMPで並列化)
2015/07/23先端GPGPUシミュレーション工学特論47
vectoradd_coop_omp.cu
#pragma omp single nowait
{
for(stm=0;stm<Stream‐CPU;stm++){
int idx = stm*N/Stream;
cudaMemcpyAsync(&a[idx], &host_a[idx], Nbytes/Stream,  
cudaMemcpyHostToDevice,stream[stm]);
cudaMemcpyAsync(&b[idx], &host_b[idx], Nbytes/Stream, 
cudaMemcpyHostToDevice,stream[stm]);
add<<< NB/Stream, NT, 0 ,stream[stm]>>>(&a[idx],&b[idx],&c[idx]);
cudaMemcpyAsync(&host_c[idx], &c[idx], Nbytes/Stream, 
cudaMemcpyDeviceToHost,stream[stm]);
}
#pragma omp for
for(int i=(Stream‐CPU)*N/Stream;i<N;i++)
host_c[i] = host_a[i] + host_b[i];
}
} //#pragma omp parallelの終端
ベクトル和(CPUをOpenMPで並列化)
2015/07/23先端GPGPUシミュレーション工学特論48
vectoradd_coop_omp.cu
cudaDeviceSynchronize();
double time_end = omp_get_wtime();
double sum=0;
for(int i=0;i<N;i++)sum+=host_c[i];
printf("%f¥n",sum/N);
printf("elapsed time = %f sec¥n",
time_end‐time_start);
cudaFreeHost(host_a);
cudaFreeHost(host_b);
cudaFreeHost(host_c);
cudaFree(a);
cudaFree(b);
cudaFree(c);
for(stm=0;stm<Stream‐CPU;stm++){
cudaStreamDestroy(stream[stm]);
}
return 0;
}
ベクトル和(CPUをOpenMPで並列化)
2015/07/23先端GPGPUシミュレーション工学特論49
vectoradd_coop_omp.cu
実行結果
2015/07/23先端GPGPUシミュレーション工学特論50
 配列の要素数 N=223
 1ブロックあたりのスレッド数 256
 ストリームの数,CPUの負荷割合,CPUスレッド数を
変えて計算
 OpenMPの関数を用いて実行時間を測定
 コンパイルにはオプション‐Xcompiler ‐fopenmpが必要
実行結果(CPUスレッドごとに最速となる条件)
2015/07/23先端GPGPUシミュレーション工学特論51
 スレッド数が多くなると実行時間が実行毎に変化
 1スレッドCPU+GPU協調版のような評価ができない
CPUスレッド数 ストリーム数 CPU負荷割合 実行時間[ms]
1 16 0.1875 11.4
2 16 0.3125 9.82
3 16 0.4375 8.32
4 16 0.4375 8.23
5 2 0.5000 10.1
6 16 0.3750 9.18
7 16 0.6875 8.89
8 16 0.6250 8.70
9 4 0.7500 8.87
10 16 0.7500 8.76
11 16 0.5625 10.3
12 16 0.4375 17.8
スレッド数の増加と
ともに,CPUの負荷
割合も増加させる
と高速化に有効
実行結果
2015/07/23先端GPGPUシミュレーション工学特論52
 CPU 1スレッド 36.4 ms
 GPU(1ストリーム) 17.5 ms
 GPU(16ストリーム) 14.0 ms
 CPUとGPUの協調 10.6 ms
 16ストリーム,CPU負荷割合0.25
 CPUとGPUの協調 8.23 ms
 CPU 4スレッド
 16ストリーム,CPU負荷割合0.4375
単一GPU,1ストリームの
2倍程度高速化
複数GPUでのストリームの利用
2015/07/23先端GPGPUシミュレーション工学特論53
 各GPUでストリームを作り,並行実行することが可能
 grouseでは
 CPUとGPU4台で非同期実行が可能
 各GPUに複数のストリームが存在し,処理を並行実行
 処理するデータの割当と管理,処理の進行状況の把握が
著しく複雑化
 注意点
 あるGPUが作成したストリームは他のGPUでは利用できない
 cudaSetDeviceでGPUを切り替えてからストリームを作成

2015年度先端GPGPUシミュレーション工学特論 第15回 CPUとGPUの協調