第7回 総和計算(Atomic演算)
長岡技術科学大学 電気電子情報工学専攻 出川智啓
今回の内容
2015/05/28先端GPGPUシミュレーション工学特論2
 総和計算
 Atomic演算
 プロファイリング
リダクション(Reduction,換算)
2015/05/28先端GPGPUシミュレーション工学特論3
 配列の全要素から一つの値を抽出
 総和,総積,最大値,最小値など
 ベクトル和や移動平均,差分法とは処理が異なる
 リダクションの特徴
 データ参照が大域的
 計算順序は交換可能
 出力が一つ
 出力は並列に計算できない
 並列化に工夫が必要 sum
a[i]
+ + + + + +
リダクション(Reduction,換算)
2015/05/28先端GPGPUシミュレーション工学特論4
 リダクションの特徴
 データ参照が大域的
 計算順序は交換可能
 出力が一つ
 出力は並列に計算できない
 並列化に工夫が必要
 ベクトル和の特徴
 データ参照が局所的
 計算が独立
 多出力
 出力は並列に計算できる
 単純に並列化可能
c[i]
a[i]
b[i]
+ + + + + +
sum
a[i]
+ + + + + +
#include<stdio.h>
#include<stdlib.h>
#define N (256)
#define Nbytes (N*sizeof(int))
int reduction(int *idata){
int sum,i;
sum = 0;
for(i=0;i<N;i++){
sum += idata[i];
}
return sum;
}
void init(int *idata){
int i;
for(i=0;i<N;i++){
idata[i] = 1;
}
}
int main(){
int *idata,sum;
idata = (int *)malloc(Nbytes);
init(idata);
sum = reduction(idata);
printf("sum = %d¥n", sum);
free(idata);
return 0;
}
総和計算のCPUプログラム
2015/05/28先端GPGPUシミュレーション工学特論5
reduction.c
ベクトル和の並列化
2015/05/28先端GPGPUシミュレーション工学特論6
 forループをスレッドの数だけ分割
 1スレッドが一つの配列添字を計算
for(i=0;i<N;i++){
c[i]=a[i]+b[i];
}
i=blockIdx.x*blockDim.x+threadIdx.x;
c[i]=a[i]+b[i];
c[i]
a[i]
b[i]
+ + + +
スレッド
0
スレッド
2
スレッド
1
スレッド
3
sum
+ + + +
総和計算の並列化
2015/05/28先端GPGPUシミュレーション工学特論7
 GPUでは並列計算が必須
 ベクトル和のようにforループを分割
for(i=0;i<N;i++){
sum += idata[i];
}
i=blockIdx.x*blockDim.x+threadIdx.x;
sum += idata[i];
?
スレッド
0
スレッド
2
スレッド
1
スレッド
3
a[i]
総和計算の並列化
2015/05/28先端GPGPUシミュレーション工学特論8
 単純な並列化は不可能
 データ競合(データレース)が発生
1.各スレッドが配列a[i]の値を読込み,
レジスタに格納
2.各スレッドが変数sumの値を読込み,
レジスタに格納したa[i]と加算
3.各スレッドがレジスタの値でsumを更新
 スレッドがメモリにアクセスした順
番によって結果が変化
 プログラム実行毎に変化 sum =     0
a[i]   1    2    3    4
sumの値(0)を参照
スレッド
0
スレッド
2
スレッド
1
スレッド
3
総和計算の並列化
2015/05/28先端GPGPUシミュレーション工学特論9
 単純な並列化は不可能
 データ競合(データレース)が発生
1.各スレッドが配列a[i]の値を読込み,
レジスタに格納
2.各スレッドが変数sumの値を読込み,
レジスタに格納したa[i]と加算
3.各スレッドがレジスタの値でsumを更新
 スレッドがメモリにアクセスした順
番によって結果が変化
 プログラム実行毎に変化 sum =     0
0+1を書込み
1
スレッド
0
スレッド
2
スレッド
1
スレッド
3
a[i]   1    2    3    4
総和計算の並列化
2015/05/28先端GPGPUシミュレーション工学特論10
 単純な並列化は不可能
 データ競合(データレース)が発生
1.各スレッドが配列a[i]の値を読込み,
レジスタに格納
2.各スレッドが変数sumの値を読込み,
レジスタに格納したa[i]と加算
3.各スレッドがレジスタの値でsumを更新
 スレッドがメモリにアクセスした順
番によって結果が変化
 プログラム実行毎に変化 sum =     0
0+2を書込み
2
スレッド
0
スレッド
2
スレッド
1
スレッド
3
a[i]   1    2    3    4
総和計算の並列化
2015/05/28先端GPGPUシミュレーション工学特論11
 単純な並列化は不可能
 データ競合(データレース)が発生
1.各スレッドが配列a[i]の値を読込み,
レジスタに格納
2.各スレッドが変数sumの値を読込み,
レジスタに格納したa[i]と加算
3.各スレッドがレジスタの値でsumを更新
 スレッドがメモリにアクセスした順
番によって結果が変化
 プログラム実行毎に変化 sum =     2
sumの値(2)を参照
スレッド
0
スレッド
2
スレッド
1
スレッド
3
a[i]   1    2    3    4
総和計算の並列化
2015/05/28先端GPGPUシミュレーション工学特論12
 単純な並列化は不可能
 データ競合(データレース)が発生
1.各スレッドが配列a[i]の値を読込み,
レジスタに格納
2.各スレッドが変数sumの値を読込み,
レジスタに格納したa[i]と加算
3.各スレッドがレジスタの値でsumを更新
 スレッドがメモリにアクセスした順
番によって結果が変化
 プログラム実行毎に変化 sum =     2
2+4を書込み
6
スレッド
0
スレッド
2
スレッド
1
スレッド
3
a[i]   1    2    3    4
総和計算の並列化
2015/05/28先端GPGPUシミュレーション工学特論13
 単純な並列化は不可能
 データ競合(データレース)が発生
1.各スレッドが配列a[i]の値を読込み,
レジスタに格納
2.各スレッドが変数sumの値を読込み,
レジスタに格納したa[i]と加算
3.各スレッドがレジスタの値でsumを更新
 スレッドがメモリにアクセスした順
番によって結果が変化
 プログラム実行毎に変化 sum =     2
2+3を書込み
5
スレッド
0
スレッド
2
スレッド
1
スレッド
3
a[i]   1    2    3    4
総和計算の並列化
2015/05/28先端GPGPUシミュレーション工学特論14
 単純な並列化は不可能
 単一スレッドの処理が正しく(逐次実行の
通りに)実行されない
 値の読み込み→加算→結果の書き込み
 Read‐Modify‐Write
 他のスレッドに割り込まれずにRead‐
Modify-Writeを実行する仕組み
が必要
 Atomic演算
 他のスレッドに任せられない最小単位の
処理
sum =     1
スレッド
0
スレッド
2
スレッド
1
スレッド
3
a[i]   1    2    3    4
Atomic演算
2015/05/28先端GPGPUシミュレーション工学特論15
 他のスレッドに割り込まれず,Read‐Modify‐Write
を実行
 あるスレッドが変数に対して独占的(排他的)に演算(四則
演算,比較等)を実行
 逐次的な処理を安全に実現
 性能が著しく低下する場合がある
 並列処理が阻害
 利用は最低限に
 Atomic演算の実行にはCompute Capability1.1
以上のGPUが必要
主なAtomic演算*
2015/05/28先端GPGPUシミュレーション工学特論16
 atomic???(更新する変数のアドレス,値)
 atomicAdd() 値の加算
 atomicSub() 値の減算
 atomicExch() 値の代入
 atomicMin() 小さい値の選択
 atomicMax() 大きい値の選択
 atomicInc() 値をインクリメント
 atomicDec() 値をデクリメント
*http://docs.nvidia.com/cuda/cuda‐c‐programming‐guide/index.html
atomicAdd
2015/05/28先端GPGPUシミュレーション工学特論17
 atomicAdd
 値の加算
 signed int, unsigned int, unsigned long long
int, float(Fermi世代以降)をサポート
__global__ void reduction(int *input, int *sum){
int i=blockIdx.x*blockDim.x + threadIdx.x;
atomicAdd(sum, input[i]);
}
アドレス 値
マルチスレッドで処理しているように見えるが,
本質的に逐次実行と同じ
総和計算の並列化
2015/05/28先端GPGPUシミュレーション工学特論18
 リダクションの並列性
 並列性なし
 i=0の処理が終了しないとi=1の処理ができない
 リダクションの並列化
 リダクションの特徴に適した並列化が必要
 Parallel Reduction
for(i=0;i<N;i++){
sum += idata[i];
}
総和計算の並列化
2015/05/28先端GPGPUシミュレーション工学特論19
 リダクションの並列化
 演算順序の変更による並列性の確保
 カスケード計算(associative fan‐in algorithm)
 カスケード計算
 加算の結合法則を利用
 乗算や比較でも利用可能
 配列要素数が2の冪乗以外では適用
に工夫が必要
 変数に浮動小数を用いる場合は値が
一致しないことがある
sum
a[i]
+ + + +
+ +
+
Parallel ReductionのGPU実装
2015/05/28先端GPGPUシミュレーション工学特論20
 実装の方針
 総和の値はポインタ渡しでカーネルに渡す
 カーネルでは返値を利用できない
 配列サイズは1ブロックに収まる大きさ(≤1024)
 ブロック内の全スレッドの同期は可能
 異なるブロック間の同期は不可能
 考え方の理解と実装を優先
 とりあえず1スレッド実装
#include<stdio.h>
#include<stdlib.h>
#define N (512)
#define Nbytes (N*sizeof(int))
#define NT (1)
#define NB (1)
__global__ void reduction0(int *idata,int *odata){
int i;
odata[0] = 0;
for(i=0;i<N;i++){
odata[0] += idata[i];
}
}
void init(int *idata){
int i;
for(i=0;i<N;i++){
idata[i] = 1;
}
}
GPUプログラム(1スレッド実装)
2015/05/28先端GPGPUシミュレーション工学特論21
reduction0.cu
int main(){
int *idata,*odata;   //GPU用変数 idata:入力,odata:出力(=総和)
int *host_idata,sum; //CPU用変数 host_idata:初期化用,sum:総和
cudaMalloc( (void **)&idata, Nbytes);
cudaMalloc( (void **)&odata, sizeof(int));
//CPU側でデータを初期化してGPUへコピー
host_idata = (int *)malloc(Nbytes);
init(host_idata);
cudaMemcpy(idata, host_idata,Nbytes, cudaMemcpyHostToDevice);
free(host_idata);
reduction0<<< NB, NT >>>(idata, odata);
//GPUから総和の結果を受け取って画面表示
cudaMemcpy(&sum, odata, sizeof(int), cudaMemcpyDeviceToHost);
printf("sum = %d¥n", sum);
cudaFree(idata);
cudaFree(odata);
return 0;
}
GPUプログラム(1スレッド実装)
2015/05/28先端GPGPUシミュレーション工学特論22
reduction0.cu
実行時間
2015/05/28先端GPGPUシミュレーション工学特論
カーネル
N=512 N=1024
実行時間
[s]
データ転送
[GB/s]
実行時間
[s]
データ転送
[GB/s]
0 54.6 0.035 121 0.032
23
プロファイリング
2015/05/28先端GPGPUシミュレーション工学特論24
 パフォーマンスカウンタの数値を出力
 プログラム実行時の挙動の記録
 ボトルネックの特定
 プロファイラ
 コマンドラインで実行するプロファイラ
 Compute Profiler
 nvprof(CUDA 5以降)
 GUIから操作するプロファイラ
 Compute Visual Profiler
 Nsight
Compute Profiler
2015/05/28先端GPGPUシミュレーション工学特論25
 CUDAの実行環境に組み込まれたプロファイラ
 環境変数を設定するだけで有効化
 使い方
1. 環境変数の設定 $ export CUDA_PROFILE=1
2. プログラムの実行 $ ./a.out
3. プロファイル結果(標準はcuda_profile_0.log)の確認
Compute Profilerの環境変数
2015/05/28先端GPGPUシミュレーション工学特論26
 CUDA_PROFILE 
 1でプロファイル実行,0でプロファイル停止
 CUDA_PROFILE_CONFIG
 プロファイル項目を記述したファイルの名前
 CUDA_PROFILE_LOG
 出力ファイル名(標準はcuda_profile_0.log)
 CUDA_PROFILE_CSV
 1で出力ファイルをCSV(カンマ区切り)形式に変更
総和のプロファイル
2015/05/28先端GPGPUシミュレーション工学特論27
 $ export CUDA_PROFILE=1
 $ nvcc ‐arch=sm_20 reduction0.cu
 $ ./a.out
 cuda_profile_0.logが出力される
 $ cat cuda_profile_0.log
プロファイル結果の確認
2015/05/28先端GPGPUシミュレーション工学特論28
 標準で出力される値
 method カーネルや関数(API)の名称
 gputime GPU上で処理に要した時間(s単位)
 cputime CPUで処理(=カーネル起動)に要した時間
実際の実行時間=cputime+gputime
 occupancy 同時実行されているWarp数と実行可能な最大
Warp数の比(一般的には1に近いほどよい)
$ cat cuda_profile_0.log
# CUDA_PROFILE_LOG_VERSION 2.0
# CUDA_DEVICE 0 Tesla M2050
# TIMESTAMPFACTOR fffff6139ae53e88
method,gputime,cputime,occupancy
method=[ memcpyHtoD ] gputime=[ 2.208 ] cputime=[ 42.000 ]
method=[ _Z10reduction0PiS_ ] gputime=[ 120.704 ] cputime=[ 15.000 ] occupancy=[ 0.021 ]
method=[ memcpyDtoH ] gputime=[ 2.592 ] cputime=[ 164.000 ]
atomic演算を利用した総和計算
2015/05/28先端GPGPUシミュレーション工学特論29
 1スレッド実装と比較
 逐次実行という点で共通
 atomic演算を使うにはコンパイルオプションが必要
 ‐arch=sm_xx
 xx={10|11|12|13|20|21|30|32|35|50|52}
 atomic演算の実行には11以上のGPUが必要
#include<stdio.h>
#include<stdlib.h>
#define N (512)
#define Nbytes (N*sizeof(int))
#define NT (256)
#define NB (N/NT)
__global__ void reduction0(int *idata, int *sum){
int i = blockIdx.x*blockDim.x + threadIdx.x;
atomicAdd(sum,idata[i]);
}
void init(int *idata){
//初期化の内容は同じなので省略
}
GPUプログラム(atomic演算)
2015/05/28先端GPGPUシミュレーション工学特論30
reduction0atomic.cu
int main(){
int *idata,*odata;   //GPU用変数 idata:入力,odata:出力(=総和)
int *host_idata,sum; //CPU用変数 host_idata:初期化用,sum:総和
cudaMalloc( (void **)&idata, Nbytes);
cudaMalloc( (void **)&odata, sizeof(int));
//CPU側でデータを初期化してGPUへコピー
host_idata = (int *)malloc(Nbytes);
init(host_idata);
cudaMemcpy(idata, host_idata,Nbytes, cudaMemcpyHostToDevice);
free(host_idata);
sum = 0;//出力用変数odataを0で初期化するためにsumに0を代入して送る
cudaMemcpy(odata, &sum, sizeof(int), cudaMemcpyHostToDevice);
reduction0<<< NB, NT >>>(idata, odata);
//GPUから総和の結果を受け取って画面表示
cudaMemcpy(&sum, odata, sizeof(int), cudaMemcpyDeviceToHost);
printf("sum = %d¥n", sum);
cudaFree(idata);
cudaFree(odata);
return 0;
}
GPUプログラム(atomic演算)
2015/05/28先端GPGPUシミュレーション工学特論31
reduction0atomic.cu
実行時間
2015/05/28先端GPGPUシミュレーション工学特論
カーネル
N=512 N=1024
実行時間
[s]
データ転送
[GB/s]
実行時間
[s]
データ転送
[GB/s]
0 54.6 0.035 121 0.032
atomic 11.8 0.162 19.7 0.194
32
総和の並列実行(Ver.1)
2015/05/28先端GPGPUシミュレーション工学特論33
 1ブロック内の複数のスレッドが並列に加算を実行
 1スレッドは「自身が読み込む配列要素iの値」と「隣の配列
要素の値」を加算
idata[i]
+ + + +
+ +
+
idata[i]
idata[i]
idata[i] odata[0]
総和の並列実行(Ver.1)
2015/05/28先端GPGPUシミュレーション工学特論34
 実装に必要な情報
 配列の要素数
 N=8
 Reductionの段数
 3(=log28)
 処理を行うスレッド数
 threadIdx.xが0と偶数
 隣の配列要素までの距離
 最初は1
 ステップが進むごとに2倍
スレッド0
+ + + +
+ +
+
Step 1
Step 2
Step 3
スレッド2 スレッド4 スレッド6
総和の並列実行(Ver.1)
2015/05/28先端GPGPUシミュレーション工学特論35
 実装に必要な情報
 配列の要素数
 N=8
 Reductionの段数
 3(=log2N)
 処理を行うスレッド
 step 1では2の倍数と0
 stepが進むごとに4の倍数,
8の倍数と変化(除数が2倍)
 隣の配列要素までの距離
 ストライド
 Step 1では1
 ステップが進むごとに2倍
スレッド0
+ + + +
+ +
+
Step 1
Step 2
Step 3
スレッド2 スレッド4 スレッド6
処理を行うスレッドを求めるための除数
=ストライド×2
#include<stdio.h>
#include<stdlib.h>
#define N (512)
#define Nbytes (N*sizeof(int))
#define NT (N)
#define NB (N/NT)
#define STEP (9) //reductionの段数
__global__ void reduction1(int *idata,int *odata){
//内容は2枚後のスライド
}
void init(int *idata){
//初期化の内容は同じなので省略
}
GPUプログラム(Ver.1)
2015/05/28先端GPGPUシミュレーション工学特論36
reduction1.cu
int main(){
int *idata,*odata;   //GPU用変数 idata:入力,odata:出力(=総和)
int *host_idata,sum; //CPU用変数 host_idata:初期化用,sum:総和
cudaMalloc( (void **)&idata, Nbytes);
cudaMalloc( (void **)&odata, sizeof(int));
//CPU側でデータを初期化してGPUへコピー
host_idata = (int *)malloc(Nbytes);
init(host_idata);
cudaMemcpy(idata, host_idata,Nbytes, cudaMemcpyHostToDevice);
free(host_idata);
reduction1<<< NB, NT >>>(idata, odata);
//GPUから総和の結果を受け取って画面表示
cudaMemcpy(&sum, odata, sizeof(int), cudaMemcpyDeviceToHost);
printf("sum = %d¥n", sum);
cudaFree(idata);
cudaFree(odata);
return 0;
}
GPUプログラム(Ver.1)
2015/05/28先端GPGPUシミュレーション工学特論37
reduction1.cu
__global__ void reduction1(int *idata,int *odata){
int i = blockIdx.x*blockDim.x + threadIdx.x;//スレッドと配列要素の対応
int tx = threadIdx.x;                        //スレッド番号
int step;   //Reductionの段数をカウントする変数
int stride; //隣の配列要素までの距離
stride = 1;
for(step=1;step<=STEP;step++){
if(tx%(2*stride)==0){
idata[i] = idata[i]+idata[i+stride];
}
__syncthreads();
stride = stride*2;
}
if(tx==0) odata[0] = idata[0];
}
GPUプログラム(Ver.1)
2015/05/28先端GPGPUシミュレーション工学特論38
reduction1.cu
odata[0]
各Stepでの総和計算(Step 1)
2015/05/28先端GPGPUシミュレーション工学特論39
 ストライドを保持する変数strideを1に
初期化
 カウンタ変数stepを1に初期化
 処理を行うスレッドを選択
 スレッド番号を2で割った剰余が0
 除数はstride*2
 スレッド番号が2の倍数と0スレッドが「自身が
読み込む配列要素iの値」と「隣の配列要素
の値」を加算
 スレッド間の同期をとる
 ストライドを2倍して次のstepに備える
+ + + +
+ +
+
Step 1
Step 2
Step 3
stride = 1;
for(step=1;step<=STEP;step++){
if(tx%(stride*2)==0){
idata[i]=idata[i]+idata[i+stride];
}
__syncthreads();
stride = stride*2;
}
スレッド0 スレッド2 スレッド4 スレッド6
odata[0]
各Stepでの総和計算(Step 2)
2015/05/28先端GPGPUシミュレーション工学特論40
 stepを2に進める
 strideを2倍(stride=2)
 処理を行うスレッドを選択
 スレッド番号をstride*2で割った剰余が0
 スレッド番号が4の倍数と0のスレッドが「自
身が読み込む配列要素iの値」と「隣
の配列要素の値」を加算
 スレッド間の同期をとる
 ストライドを2倍して次のstepに備える
+ + + +
+ +
+
Step 1
Step 2
Step 3
stride = 1;
for(step=1;step<=STEP;step++){
if(tx%(stride*2)==0){
idata[i]=idata[i]+idata[i+stride];
}
__syncthreads();
stride = stride*2;
}
スレッド0 スレッド4
odata[0]
各Stepでの総和計算(Step 3)
2015/05/28先端GPGPUシミュレーション工学特論41
 stepを3に進める
 strideを2倍(stride=4)
 処理を行うスレッドを選択
 スレッド番号をstride*2で割った剰余が0
 スレッド番号が8の倍数と0のスレッドが「自
身が読み込む配列要素iの値」と「隣
の配列要素の値」を加算
 スレッド間の同期をとる
 ストライドを2倍して次のstepに備える
 stepがlog2Nに達するまで繰り返す
+ + + +
+ +
+
Step 1
Step 2
Step 3
stride = 1;
for(step=1;step<=STEP;step++){
if(tx%(stride*2)==0){
idata[i]=idata[i]+idata[i+stride];
}
__syncthreads();
stride = stride*2;
}
スレッド0
各Stepでの総和計算(結果の書き出し)
2015/05/28先端GPGPUシミュレーション工学特論42
 スレッド0が総和を出力用変数odataに
書き込んで終了
+ + + +
+ +
+
Step 1
Step 2
Step 3
if(tx==0) odata[0] = idata[0];
odata[0]
スレッド0
実行時間
2015/05/28先端GPGPUシミュレーション工学特論
カーネル
N=512 N=1024
実行時間
[s]
データ転送
[GB/s]
実行時間
[s]
データ転送
[GB/s]
0 54.6 0.035 121 0.032
atomic 11.8 0.162 19.7 0.194
1 9.73 0.196 12.8 0.297
43
Ver.1の問題点
2015/05/28先端GPGPUシミュレーション工学特論44
美しくない
Ver.1の問題点
2015/05/28先端GPGPUシミュレーション工学特論45
 入力データが変更される
 スレッド番号が奇数のスレッドが何も処理をしない
 無駄が多い
 strideがループ外で初期化
 strideがループ毎に更新
 対策
 for文を利用して記述を簡略化
 整数の2倍はシフト演算で対応可能
__global__ void reduction1(int *idata,int *odata){
int i = blockIdx.x*blockDim.x + threadIdx.x;//スレッドと配列要素の対応
int tx = threadIdx.x;                        //スレッド番号
int step;   //Reductionの段数をカウントする変数
int stride; //隣の配列要素までの距離
//ストライドを2倍
for(step=1,stride = 1;step<=STEP;step++, stride = stride*2;){
if(tx%(stride*2)==0){
idata[i] = idata[i]+idata[i+stride];
}
__syncthreads();
}
if(tx==0) odata[0] = idata[0];
}
GPUプログラム(Ver.1.1)
2015/05/28先端GPGPUシミュレーション工学特論46
reduction1shift.cu
シフト演算
2015/05/28先端GPGPUシミュレーション工学特論47
 C言語は整数型変数に対するシフト演算子を定義
 左シフト << (上位ビットは棄却,下位ビットは0を格納)
 右シフト >> (下位ビットは棄却,上位ビットは0を格納)
 シフト演算の代入演算子
 左シフト <<=
 右シフト >>=
int a=1;   //0000 0000 0000 00012
a = a<<2; //0000 0000 0000 01002
a = a>>1; //0000 0000 0000 00102
int a=1;
a <<=2; //a = a << 2 と同じ
a >>=1; //a = a >> 1 と同じ
__global__ void reduction1(int *idata,int *odata){
int i = blockIdx.x*blockDim.x + threadIdx.x;//スレッドと配列要素の対応
int tx = threadIdx.x;                        //スレッド番号
int step;   //Reductionの段数をカウントする変数
int stride; //隣の配列要素までの距離
//ストライドを左に一つビットシフト(値が2倍になる)
for(step=1,stride = 1;step<=STEP;step++, stride<<=1;){
if(tx%(stride*2)==0){
idata[i] = idata[i]+idata[i+stride];
}
__syncthreads();
}
if(tx==0) odata[0] = idata[0];
}
GPUプログラム(Ver.1.1)
2015/05/28先端GPGPUシミュレーション工学特論48
reduction1shift.cu
実行時間
2015/05/28先端GPGPUシミュレーション工学特論
カーネル
N=512 N=1024
実行時間
[s]
データ転送
[GB/s]
実行時間
[s]
データ転送
[GB/s]
1 9.73 0.196 12.8 0.297
1.1 9.70 0.197 12.8 0.298
49
Ver.1.1の問題点
2015/05/28先端GPGPUシミュレーション工学特論50
 入力データが変更される
 スレッド番号が奇数のスレッドが何も処理をしない
 無駄が多い
 パラメータの変更が2カ所
 stepはループカウンタとしてのみ利用
 ループカウンタはstrideで代用可能
 最終stepでは,strideが配列要素数のN/2
 strideがN/2(=blockDim.x/2)より大きくなるとループ
を中断
 無駄を排除してカーネルを簡略化
__global__ void reduction1(int *idata,int *odata){
int i = blockIdx.x*blockDim.x + threadIdx.x;//スレッドと配列要素の対応
int tx = threadIdx.x;                        //スレッド番号
int step;   //Reductionの段数をカウントする変数 //#define STEPも削除
int stride; //隣の配列要素までの距離
for(stride = 1; stride <= blockDim.x/2; stride<<=1){
if(tx%(2*stride) == 0){
idata[i] = idata[i]+idata[i+stride];
}
__syncthreads();
}
if(tx==0) odata[0] = idata[0];
}
GPUプログラム(Ver.1.2)
2015/05/28先端GPGPUシミュレーション工学特論51
reduction1stride.cu
odata[0]
各Stepでの総和計算(Step 1)
2015/05/28先端GPGPUシミュレーション工学特論52
 ストライドを保持する変数strideを1に
初期化
 処理を行うスレッドを選択
 スレッド番号を2で割った剰余が0
 除数はstride*2
 スレッド番号が2の倍数と0スレッドが「自身が
読み込む配列要素iの値」と「隣の配列要素
の値」を加算
 スレッド間の同期をとる
 ストライドを2倍し,ストライドがN/2以下
ならループを継続
+ + + +
+ +
+
stride
が1の段
for(stride = 1; stride <= blockDim.x/2;
stride<<=1){
if(tx%(2*stride) == 0){
idata[i]=idata[i]+idata[i+stride];
}
__syncthreads();
}
スレッド0 スレッド2 スレッド4 スレッド6
odata[0]
各Stepでの総和計算(Step 2)
2015/05/28先端GPGPUシミュレーション工学特論53
 処理を行うスレッドを選択
 スレッド番号をstride*2で割った剰余が0
 スレッド番号が4の倍数と0のスレッドが「自
身が読み込む配列要素iの値」と「隣
の配列要素の値」を加算
 スレッド間の同期をとる
 ストライドを2倍し,ストライドがN/2以下
ならループを継続
 strideが4になるのでstride=N/2
 ※NとblockDim.xが等しくなるように設定し
ているため
+ + + +
+ +
+
stride
が2の段
for(stride = 1; stride <= blockDim.x/2;
stride<<=1){
if(tx%(2*stride) == 0){
idata[i]=idata[i]+idata[i+stride];
}
__syncthreads();
}
スレッド0 スレッド4
odata[0]
各Stepでの総和計算(Step 3)
2015/05/28先端GPGPUシミュレーション工学特論54
 処理を行うスレッドを選択
 スレッド番号をstride*2で割った剰余が0
 スレッド番号が8の倍数と0のスレッドが「自
身が読み込む配列要素iの値」と「隣
の配列要素の値」を加算
 スレッド間の同期をとる
 ストライドを2倍し,ストライドがN/2以下
ならループを継続
 strideが8になるのでstride>N/2となって
ループを抜ける
+ + + +
+ +
+stride
が4の段
for(stride = 1; stride <= blockDim.x/2;
stride<<=1){
if(tx%(2*stride) == 0){
idata[i]=idata[i]+idata[i+stride];
}
__syncthreads();
}
スレッド0
各Stepでの総和計算(結果の書き出し)
2015/05/28先端GPGPUシミュレーション工学特論55
 スレッド0が総和を出力用変数odataに
書き込んで終了
+ + + +
+ +
+
if(tx==0) odata[0] = idata[0];
odata[0]
スレッド0
実行時間
2015/05/28先端GPGPUシミュレーション工学特論
 実行時間が増加
 forループに対する最適化が抑制された?
 #pragma unrollでループ展開してもLoop was not unrolled, 
cannot deduce loop trip countというメッセージが出力
 Nを変更するとSTEPも変更されるなら,バージョン1.1でもよい
カーネル
N=512 N=1024
実行時間
[s]
データ転送
[GB/s]
実行時間
[s]
データ転送
[GB/s]
1 9.73 0.196 12.8 0.297
1.1 9.70 0.197 12.8 0.298
1.2 12.8 0.149 18.3 0.209
56
Ver.1.2の問題点と改良
2015/05/28先端GPGPUシミュレーション工学特論57
 二つの問題が依然として存在
 入力データが変更される
 スレッド番号が奇数のスレッドが何も処理をしない
 共有メモリをキャッシュとして利用
 共有メモリを変更してもグローバルメモリは変化しない
 全スレッドがグローバルメモリにコアレスアクセスしてデー
タを読み込み,共有メモリへコピー
総和の並列実行(Ver.2)
2015/05/28先端GPGPUシミュレーション工学特論58
 全スレッドがグローバルメ
モリから共有メモリへデー
タをコピー
 共有メモリ上で総和を計算
 1ブロック内の複数のスレッ
ドが並列に加算を実行
 1スレッドは「自身が読み込む
配列要素iの値」と「隣の配列
要素の値」を加算
s_idata[i]
+ + + +
+ +
+
s_idata[i]
s_idata[i]
s_idata[i]odata[0]
idata[i]
総和の並列実行(Ver.2)
2015/05/28先端GPGPUシミュレーション工学特論59
 実装に必要な情報
 共有メモリサイズ(要素数)
 総和を計算するデータの
サイズと同じ
 その他はVer.1.2と同じ
Step 1
Step 2
Step 3
+ + + +
+ +
+
#include<stdio.h>
#include<stdlib.h>
#define N (512)
#define Nbytes (N*sizeof(int))
#define NT (N)
#define NB (N/NT)
__global__ void reduction2(int *idata,int *odata){
//内容は2枚後のスライド
}
void init(int *idata){
//初期化の内容は同じなので省略
}
GPUプログラム(Ver.2)
2015/05/28先端GPGPUシミュレーション工学特論60
reduction2.cu
int main(){//Ver.1から変更点無し
int *idata,*odata;   //GPU用変数 idata:入力,odata:出力(=総和)
int *host_idata,sum; //CPU用変数 host_idata:初期化用,sum:総和
cudaMalloc( (void **)&idata, Nbytes);
cudaMalloc( (void **)&odata, sizeof(int));
//CPU側でデータを初期化してGPUへコピー
host_idata = (int *)malloc(Nbytes);
init(host_idata);
cudaMemcpy(idata, host_idata,Nbytes, HtoD);
free(host_idata);
reduction2<<< NB, NT >>>(idata, odata);
//GPUから総和の結果を受け取って画面表示
cudaMemcpy(&sum, odata, sizeof(int), DtoH);
printf("sum = %d¥n", sum);
cudaFree(idata);
cudaFree(odata);
return 0;
}
GPUプログラム(Ver.2)
2015/05/28先端GPGPUシミュレーション工学特論61
reduction2.cu
__global__ void reduction2(int *idata,int *odata){
int i = blockIdx.x*blockDim.x + threadIdx.x;//スレッドと配列要素の対応
int tx = threadIdx.x;                        //スレッド番号
int stride; //”隣”の配列要素までの距離
__shared__ volatile int s_idata[N]; //共有メモリの宣言
s_idata[i] = idata[i]; //グローバルメモリから共有メモリへデータをコピー
__syncthreads(); //共有メモリのデータは全スレッドから参照されるので同期を取る
//総和計算はVer.1.2(reduction1stride.cu)と同じ
for(stride = 1; stride <= blockDim.x/2; stride<<=1){
if(tx%(2*stride) == 0){
s_idata[i] = s_idata[i]+s_idata[i+stride];
}
__syncthreads();
}
if(tx==0) odata[0] = s_idata[0];
}
GPUプログラム(Ver.2)
2015/05/28先端GPGPUシミュレーション工学特論62
reduction2.cu
volatile修飾子
2015/05/28先端GPGPUシミュレーション工学特論63
 コンパイラの最適化を抑制
 複数のスレッドからアクセスされる変数に対する最適化
 コンパイラが不要と判断して処理を削除
 複数スレッドが変数の値をプライベートな領域にコピーし
て書き戻さない等
 このバージョンでは不要だが後々必要
 さらに最適化する場合は必須
volatile int s_idata[NT];
総和計算
2015/05/28先端GPGPUシミュレーション工学特論64
 ブロック内の全スレッドがグローバルメ
モリへアクセスし,データを共有メモリへ
コピー
 全スレッドが1回は何らかの処理を行う
 総和計算はVer.1.2と同じ
 取り扱う配列がidata[]からs_idata[]に
変更されているだけ
+ + + +
strideが
1の段
s_idata[i] = idata[i];
__syncthreads();
idata[i]
s_idata[i]
実行時間
2015/05/28先端GPGPUシミュレーション工学特論
カーネル
N=512 N=1024
実行時間
[s]
データ転送
[GB/s]
実行時間
[s]
データ転送
[GB/s]
0 54.6 0.035 121 0.032
atomic 11.8 0.162 19.7 0.194
1 9.73 0.196 12.8 0.297
2 10.9 0.175 15.9 0.240
65
Ver.2の問題点と改良
2015/05/28先端GPGPUシミュレーション工学特論66
 取り扱える配列サイズが1ブロックで処理できる大きさ
 1スレッドが配列の1要素を担当
 1ブロックあたりのスレッド数の上限(≤1024)
 複数のブロックで総和を計算
 ブロック1が配列要素 0~1023の和を計算
 ブロック2が配列要素1024~2047の和を計算...
 得られた部分和をアトミック演算で加算して総和を計算
総和の並列実行(Ver.3)
2015/05/28先端GPGPUシミュレーション工学特論67
 総和を2段階に分けて実行
1. 各ブロックが配列の部分和を計算
2. 各ブロックが計算した部分和から総和を計算
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+
+
+
+
総和の並列実行(Ver.3)
2015/05/28先端GPGPUシミュレーション工学特論68
 ブロック内の全スレッドが
グローバルメモリから共有
メモリへデータをコピー
 共有メモリ上で総和を計算
 1ブロック内の複数のス
レッドが並列に加算を実行
 主な変更点
 共有メモリを参照する添字
 アトミック演算の追加
s_idata[tx]
+ + + +
+ +
s_idata[tx]
idata[i]
+
sum
blockIdx.x=0 blockIdx.x=1
+
総和の並列実行(Ver.3)
2015/05/28先端GPGPUシミュレーション工学特論69
 実装に必要な情報
 部分和の記録場所
 s_idata[0]
 共有メモリ用の配列添字と
グローバルメモリ用の配列
添字の区別
s_idata[tx]
+ + + +
+ +
s_idata[tx]
idata[i]
blockIdx.x=0
+
+
blockIdx.x=1
sum
#include<stdio.h>
#include<stdlib.h>
#define N (1024*8)
#define Nbytes (N*sizeof(int))
#define NT (256)
#define NB (N/NT) //1より大きくなる
__global__ void reduction3(int *idata,int *odata){
//内容は2枚後のスライド
}
void init(int *idata){
//初期化の内容は同じなので省略
}
GPUプログラム(Ver.3)
2015/05/28先端GPGPUシミュレーション工学特論70
reduction3.cu
int main(){
int *idata,*odata;   //GPU用変数 idata:入力,odata:出力(=総和)
int *host_idata,sum; //CPU用変数 host_idata:初期化用,sum:総和
cudaMalloc( (void **)&idata, Nbytes);
cudaMalloc( (void **)&odata, sizeof(int));
//CPU側でデータを初期化してGPUへコピー
//...省略...
sum = 0;//出力用変数odataを0で初期化するためにsumに0を代入して送る
cudaMemcpy(odata, &sum, sizeof(int), cudaMemcpyHostToDevice);
reduction3<<< NB, NT >>>(idata, odata);
//GPUから部分和を受け取って総和を計算
cudaMemcpy(&sum, odata, sizeof(int), cudaMemcpyDeviceToHost);
printf("sum = %d¥n", sum);
cudaFree(idata);
cudaFree(odata);
return 0;
}
GPUプログラム(Ver.3)
2015/05/28先端GPGPUシミュレーション工学特論71
reduction3.cu
__global__ void reduction3(int *idata,int *odata){
int i = blockIdx.x*blockDim.x + threadIdx.x;//スレッドと配列要素の対応
int tx = threadIdx.x;                        //スレッド番号
int stride; //”隣”の配列要素までの距離
__shared__ volatile int s_idata[NT]; //共有メモリの宣言
s_idata[tx] = idata[i]; //グローバルメモリから共有メモリへデータをコピー
__syncthreads(); //共有メモリのデータは全スレッドから参照されるので同期を取る
//総和計算はバージョン1.2(reduction1stride.cu)と同じ
for(stride = 1; stride <= blockDim.x/2; stride<<=1){
if(tx%(2*stride) == 0){
s_idata[tx] = s_idata[tx]+s_idata[tx+stride];
}
__syncthreads();
}
if(tx==0) atomicAdd(odata,s_idata[tx]);
}
GPUプログラム(Ver.3)
2015/05/28先端GPGPUシミュレーション工学特論72
reduction3.cu
総和計算
2015/05/28先端GPGPUシミュレーション工学特論73
 共有メモリサイズは1ブロックが処理する
データの数と同じ
 ブロック内の全スレッドがグローバルメ
モリへアクセスし,データを共有メモリへ
コピー
 総和計算はバージョン1.2と同じ
 グローバルメモリにアクセスするための配列
添字i
 共有メモリにアクセスするための配列添字tx
+ + + +
strideが
1の段
__shared__ volatile int s_idata[NT];
s_idata[tx] = idata[i];
__syncthreads();
idata[i]
s_idata[tx]
i=  0    1    2   3   4   5   6    7
threadIdx.x=  0    1    2 3 0   1   2 3
+
+
各ブロックでの部分和の計算
2015/05/28先端GPGPUシミュレーション工学特論74
 ストライドを保持する変数strideを1に
初期化
 処理を行うスレッドを選択
 スレッド番号を2で割った剰余が0
 除数はstride*2
 スレッド番号が2の倍数と0スレッドが「自身が
読み込む配列要素iの値」と「隣の配列要素
の値」を加算
 スレッド間の同期をとる
 ストライドを2倍し,ストライドがN/2以下
ならループを継続
+ + + +
+ +
strideが
1の段
for(stride = 1; stride <= blockDim.x/2;
stride<<=1){
if(tx%(2*stride) == 0){
s_idata[tx]=s_idata[tx]+s_idata[tx+stride];
}
__syncthreads();
}
スレッド0 スレッド2 スレッド0 スレッド2
+
+
各ブロックでの部分和の計算
2015/05/28先端GPGPUシミュレーション工学特論75
 処理を行うスレッドを選択
 スレッド番号をstride*2で割った剰余が0
 スレッド番号が4の倍数と0のスレッドが「自
身が読み込む配列要素iの値」と「隣
の配列要素の値」を加算
 スレッド間の同期をとる
 ストライドを2倍し,ストライドがN/2以下
ならループを継続
 strideが4になるのでstride > 
blockDim.x/2となってループを抜ける
+ + + +
+ +strideが
2の段
for(stride = 1; stride <= blockDim.x/;
stride<<=1){
if(tx%(2*stride) == 0){
s_idata[i]=s_idata[i]+s_idata[i+stride];
}
__syncthreads();
}
スレッド0 スレッド0
for(stride = 1; stride <= blockDim.x/2;
stride<<=1){
if(tx%(2*stride) == 0){
s_idata[tx]=s_idata[tx]+s_idata[tx+stride];
}
__syncthreads();
}
各ブロックでの部分和の計算(結果の出力)
2015/05/28先端GPGPUシミュレーション工学特論76
 各ブロックのスレッド0がatomicAdd()
を呼び出す
 各ブロックのs_idata[0]に計算された
部分和が格納
 各ブロックのスレッド0が,部分和をグローバルメ
モリsumへ排他的に加算
+ + + +
+ +
if(tx==0) atomicAdd(sum,s_idata[tx]);
sum
スレッド0 スレッド0
+
+
実行時間
2015/05/28先端GPGPUシミュレーション工学特論
 配列要素数が少なくてもスレッド数を適切に決定できる
 配列要素数を大きくするにつれてデータ転送レートが向上
 atomic演算はそこまで遅くない?
 カーネルの最適化が十分なのか検討が必要
77
N(NT) 実行時間 [s] データ転送[GB/s]
29(256) 8.80 0.217
210(256) 8.83  0.432
212(256) 9.47 1.61
214(256) 15.1 4.04
216(256) 43.2 5.65
218(256) 155 6.30
220(256) 596 6.55
カーネルVer.2の結果
10.9 ms, 0.175 GB/s
カーネルVer.2の結果
15.9 ms, 0.240 GB/s
double型に対するatmicAdd
(第15回目の授業で紹介)
__device__ double atomicAdd(double *address, const double val){
unsigned long long int *address_as_ull = (unsigned long long int *)address;
unsigned long long int old = *address_as_ull;
unsigned long long int assumed;
do{
assumed = old;
old = atomicCAS(address_as_ull, assumed, __double_as_longlong(val
+ __longlong_as_double(assumed)));
}while(assumed != old);
return __longlong_as_double(old);
}
double型に対するatomicAdd()
2015/07/23先端GPGPUシミュレーション工学特論81
 double型を返すデバイス関数を作成
 コンパイラが引数の型を判断して適切な関数を選択
 関数のオーバーロード
#include<stdio.h>
#include<stdlib.h>
#define TYPE double
#define N (512)
#define Nbytes (N*sizeof(TYPE))
#define NT (256)
#define NB (N/NT)
__device__ double atomicAdd(double *address, const double val){
unsigned long long int *address_as_ull = (unsigned long long int *)address;
unsigned long long int old = *address_as_ull;
unsigned long long int assumed;
do{
assumed = old;
old = atomicCAS(address_as_ull, assumed, __double_as_longlong(val
+ __longlong_as_double(assumed)));
}while(assumed != old);
return __longlong_as_double(old);
}
double型に対するatomicAdd()
2015/07/23先端GPGPUシミュレーション工学特論82
reduction3double.cu
__global__ void reduction3(TYPE *idata, TYPE *sum){
int i = blockIdx.x*blockDim.x + threadIdx.x;
int tx = threadIdx.x;
int stride;
__shared__ volatile TYPE s_idata[NT];
s_idata[tx] = idata[i];
__syncthreads();
for(stride = 1; stride <= blockDim.x/2; stride<<=1){
if(tx%(2*stride) == 0){
s_idata[tx] = s_idata[tx]+s_idata[tx+stride];
}
__syncthreads();
}
if(tx==0) atomicAdd(sum,s_idata[tx]);
}
double型に対するatomicAdd()
2015/07/23先端GPGPUシミュレーション工学特論83
reduction3double.cu
void init(TYPE *idata){
int i;
for(i=0;i<N;i++){
idata[i] = 1;
}
}
int main(){
TYPE *idata,*odata;
TYPE *host_idata,sum;
cudaMalloc( (void **)&idata, Nbytes);
cudaMalloc( (void **)&odata, sizeof(TYPE));
host_idata = (TYPE *)malloc(Nbytes);
init(host_idata);
cudaMemcpy(idata, host_idata,Nbytes, cudaMemcpyHostToDevice);
free(host_idata);
double型に対するatomicAdd()
2015/07/23先端GPGPUシミュレーション工学特論84
reduction3double.cu
sum = 0;
cudaMemcpy(odata, &sum,sizeof(TYPE), cudaMemcpyHostToDevice);
reduction3<<< NB, NT >>>(idata, odata);
cudaMemcpy(&sum, odata, sizeof(TYPE), cudaMemcpyDeviceToHost);
printf("array size = %d, sum = %f¥n", N, sum);
cudaFree(idata);
cudaFree(odata);
return 0;
}
double型に対するatomicAdd()
2015/07/23先端GPGPUシミュレーション工学特論85
reduction3double.cu

2015年度先端GPGPUシミュレーション工学特論 第7回 総和計算(Atomic演算)