ネットワークシステム研究室
指導教員 坂本 直志
13EC101 後藤 徹哉
近年、PGUを汎用的に使用する事が可能になった事や、計算性能の飛躍的な向上からPGUによる数値流体解析の研究が盛んに行われている。従来並列コンピュータはCPUを使った非常に大掛かりなクラスタで構成されて来た。その最もたる物がスーパーコンピュータである。しかし現在、PGUはその内部に非常に多数のコアを持つ様になり、1つのデバイスそのものが並列コンピュータと言えるまでになっている。こうした状況の中で、従来スーパーコンピュータで計算されていた処理をPGUによって行わせる動きが活発になっている。
本研究では流体の物理シミュレータの高速化の手法として、GPPGUを利用した流体シミュレーションプログラムを動的に生成する事の有用性について評価する。
流体とは、水や空気と言った力を加えると変形する連続体の事である。流体は小さな力で形が大きく変化する他体積もまた変化し、流体の粘性や摩擦等もその運動に影響を与える。この流体の運動を研究する為に流体力学と呼ばれる力学の1分野が存在する。
流体力学における流体の運動はナビエ-ストークス方程式によって記述される。ナビエ-ストークス方程式は解析する流体の性質によっていくつかの種類があるが、非圧縮性の流れの場合は式\eqref{NavierStokes}で表される。
ナビエ-ストークス方程式の一般解は求められていない[1]為、流体の解析では主に数値解析を用いて問題を解く。特に流体を数値解析する場合数値流体力学と呼ばれる。
数値流体力学(CFD : Computational Fluid Dynamics)とは流体に関する方程式をコンピュータで計算する事により数値的に流れを求める手法の事である。CFDでは流体の方程式をコンピュータで計算する為、空間や時間と言った連続的な値を離散化して解を求める。離散化された空間ではその一つ一つの離散点に同じ方程式が適用される。
CFDは主に陰的な解法によって計算される。陰的な解法とは計算する方程式の従属変数が他の方程式によって定義されるような場合に反復計算などを行って解を求める解法である。
GPUとはGraphics Processing Unitのことであり、コンピュータの画面表示を司る装置である。昔はCPUのアクセスできるメモリの一部を用いて画面のイメージを表示していたが、現在のコンピュータでは図形などの描画用のAPIが用意され、PGUが図形などの表示を担当する。そのため、図形表示などの特定の処理を高速に実行できるように画像処理に最適なハードウェアが構築されている。具体的にはPGUは1つの命令で同時に多数のデータを処理するSIMD(Single Instruction Mdltiple Data)と呼ばれる方法で計算を行う。もともとPGUは2Dの描画機能をCPUから切り離しCPUの負荷を低減させる為の物であったが、次第に3Dグラフィックスの描画高速化としてベクトルや行列の演算を行えるよう拡張されていった。ベクトルや行列等の計算が行えるようになると、その計算能力をグラフィックス目的だけでなく汎用的に使用するニーズが生まれてきた。こうしてGPPGUが生まれた。
GPGPU(General-Purpose computing on Graphics Processing Units)とは、PGUを汎用計算に応用する技術の事である。PGUの得意とするベクトルの並列処理をあらゆるデータに対し行えるようにする物で、単純な命令を多量のデータに並列に適応させる事で高速化を図る。なお、GPPGUはあらゆる処理を高速化できるわけではない。例えば一般にGPPGUでは条件分岐は計算速度を損なわせる。これはPGUの演算器に条件分岐を最適化する投機実行や分岐予測と言った機能が無い[2]為である。また、PGUはメモリアクセスの際一定量をまとめて読み込む仕組みの為、アクセスするメモリ位置が連続していない場合は何度もメモリ転送が発生し速度が低下する。したがって、特定の目的をGPPGUで高速化できるかどうかは重要な研究課題である。
CFDとGPPGUは非常に親和性が高い。これは空間の離散化と言う点にある。CFDでは解析する空間領域を離散化し、各離散点ごとに流体の方程式を計算するが、CPUの場合は2017年現在での並列度は多くて数十なので、個々のコアで全離散点を逐次的に計算するため時間がかかる。一方GPPGUの場合離散点の計算を並列に行うのにSIMD計算が使え、またコア数も数千から数万となるためこの計算時間を大幅に減らすことが出来る。
本研究では流体の物理シミュレータの高速化手法として、GPPGUを利用した流体シミュレーションプログラムを与えられた格子数やスレッド数などで最適化を施し動的に生成するプログラムを作成した。高速化の手法としてプログラムの動的生成が有用であるか評価する。
2章では本研究で使用したGPPGU環境であるCUDAの基礎的な用語や技術を解説する。
3章では本研究の流体シミュレーション方法として使用した格子ボルツマン法について述べる。
4章では本研究で作成した流体シミュレータのシミュレーション内容について述べる。
5章では本研究の内容についてCPUコードによるLBMの実装からCUDAでの実装、そして本研究で行った高速化手法の解説する。
6章では本研究の結果を示し、評価を行う。
7章では本研究のまとめを述べる。
8章では計測値の生データを示し、本研究で作成したプログラムとその説明を書く。
CUDA(Compute Unified Device Architecture)とは、NVIDIA社が提供するハードウェア仕様、プログラミングモデル、SDKやAPI等で構成されるGPPGUプログラミングの開発環境の事で、C言語を使用して並列プログラムを作成することが出来る。
CUDAにはSMX(Streaming Mdltiprocessor)と呼ばれる装置が多数搭載されている。このSMXにはキャッシュやレジスタ、そして多数のCUDAコアと呼ばれる演算装置等が搭載されている。CUDAコアは計算を行う最小単位と言え、このコアを最大限使用する事がPGUの性能を最大限発揮させる事に繋がる。なおSMX及びSMX内のCUDAコアの数は製品によって異なる。
CUDAにおいて並列プログラムを作成する場合、並列実行するプログラムをカーネルと呼び、関数として記述される。カーネル内の命令はCUDAコアによって実行される。CUDAでは多数のコアそれぞれが同じカーネルを実行することによって並列処理を行っている。
CUDAではカーネルを管理する為にスレッド階層と呼ばれる階層的な管理方法を提供している。スレッド階層の概要図を図aaaに示す。
スレッドとはカーネルの実行単位である。CUDAでは非常に多数のスレッドを作る事が出来るが、ブロック当たりのスレッド数は上限が存在する。
ブロックはスレッドを束ねたものである。1つのブロックは1つのSMXに割り当てられる。なお1ブロック内に含める事の出来るスレッド数は製品によって異なる。ブロックは最大3次元でスレッドを管理する事が出来る。またブロック内のスレッドは共有メモリを通してデータのやり取りを行う事が出来る。
グリッドはブロックを束ねたものである。グリッドも最大3次元でブロックを管理する事が出来る。ただし、1つの次元で格納する事の出来るブロック数は、製品によって異なるが、上限が存在する。
なおCUDAではグリッドの上位にデバイスと呼ばれる概念が存在する。1つのデバイスは1つのPGUを意味するため、マルチPGU環境では複数のデバイスを持つ事になる。
CUDAではPGU上で動作するそれぞれのスレッドに対しスレッドID(1ブロック内のスレッドの通し番号)とブロックID(1グリッド内のブロックの通し番号)を付与する為、この値を用いてデータの選択を行う事が出来る。これらはカーネル内で組み込み変数として参照できる。
スレッドはWarpと呼ばれる単位ごとに制御される。1Warpは32スレッドであり、Warp内のスレッドはすべて同じ命令を実行する。つまり、Warp内のスレッドはすべて同じカーネルを実行する。また半Warpは16スレッドの事を言う。
デバイスとは前述のとおり1つのPGUを意味する。一方ホストとはPGUと接続されているCPU(あるいはCPU上で動くカーネルを管理するプログラム)の事を言う。CUDAではホスト側からデバイスへカーネルの実行命令を出すと非同期にデバイスがカーネルの処理を始め、ブロッキングされる事無くホスト側へ処理が戻る。よってホスト側ではデバイス側の処理終了を待つこと無く、他のタスクを実行することが出来る。
CUDAにはいくつものメモリが存在する。PGUのチップから見たメモリ構造を図aに、スレッド階層から見たメモリ構造を図aaに示す。
図aaaaを見るとOn ChipとOff Chipとある。これはメモリがPGUのチップのどこに存在しているかを示す。On Chipの場合はチップ内部に、Off Chipの場合はチップ外部に存在している。On Chipメモリの場合は小容量ではあるもののCUDAコアからは高速にアクセスする事が出来る。ただしCPUからの読み書きはできない。一方Off Chipは基本的に大容量であるがCUDAコアからのアクセスはOn Chipメモリよりも低速になる。しかしCPUからのアクセスを行う事が出来る。
レジスタはCUDAコアの最も近くに存在するメモリで非常に高速に読み書きすることが出来る。アクセスする事が出来るのはそのレジスタを割り当てられたスレッドのみとなる。またレジスタを使用するのはカーネル内のローカル変数である。
CUDAではブロック(SMX)内のレジスタを、スレッド(CUDAコア)に分配する。このため、ブロック内のスレッド数が少なければ1つのスレッドが使用する事の出来るレジスタ数は増える。逆にブロック内のスレッド数が多ければ少なくなる。レジスタの数が足りなくなった場合はローカルメモリへデータは退避される。ローカルメモリはレジスタよりも非常に低速なため、レジスタ不足は実行速度の低下を招くので回避する必要がある。その方法はカーネルで使用するローカル変数を減らすか、ブロック内のスレッド数を減らす必要がある。
共有メモリはOn Chipのメモリで同一ブロック内のスレッドならばどれでも読み書きする事の出来るメモリである。On Chipメモリなので高速にアクセスすることが出来、スレッド間のデータ交換等に使用される。
ローカルメモリはOff Chipのメモリでレジスタや共有メモリよりもアクセス速度は低速になる。ローカルメモリにアクセス出来るのは同じアドレスにつき1つのスレッドまでとなる。ローカルメモリは足りなくなったレジスタを補う為のメモリでCPUやカーネルから明示的にアクセスする事は出来ない。ローカルメモリは図arch-memoryの様に2か所に存在していて、1つはL2キャッシュ、もう一つはグローバルメモリである。最もメモリアクセスが遅いグローバルメモリを使用する前に一段キャッシュ機構を組み込むことでアクセスの高速化を図っている。L2キャッシュが一杯だったりキャッシュミスを起こすとグローバルメモリにアクセスする。
コンスタントメモリはOn Chipのキャッシュを持つ読み込み専用メモリである。デバイスからの値の書き込みはできないが、CPUから書き込む事が出来る。コンスタントメモリでは値の読み込みはまずOn Chipのキャッシュを通して行われるため、半Warpのすべてのスレッドがコンスタントメモリ内の同じ場所を参照する場合はグローバルメモリを使用する場合に比べて高速に値をロードできる。コンスタントメモリの容量はデバイスによって異なるが、本研究で用いたNVIDIA GeForce TITANでは64KBとなっている。
グローバルメモリはデバイスの中で最も大容量のメモリである。デバイス、ホストの両方で読み書きを行う事が出来る。一方メモリアクセスの速度は最も遅く、実行時間に大きな影響を与える。グローバルメモリアクセスの高速化方法にコアレスアクセスと呼ばれるものがある。これは連続したスレッドがグローバルメモリ内の連続した領域にアクセスする事を言う。CUDAではグローバルメモリアクセスの際連続した領域を一度に読み込むため、連続したスレッドがグローバルメモリの連続した領域にアクセスする場合一度のアクセスで読み込みを行う事が出来る。一方不連続な領域にアクセスする場合、何度もメモリ読み込みを行う為速度が低下する。
グローバルメモリのアクセスを減らすための手段としては、カーネルの最初で値をキャッシュしておく方法が挙げられるがこの場合カーネル内で使用するレジスタの数が増えるため場合によっては一度に並列処理できるスレッド数が減り、結果的に速度が低下する事もある。
格子ボルツマン法(Lattice Boltzmann Method、以下LBM)はCFDの一種である。これは空間内の気体粒子の分布を離散化された空間で時間発展的に解くことにより、気体の動きを解析する手法の事である。LBMでは空間を有限個の格子に分割し、その格子点の仮想的な粒子の分布を周囲の格子点を用いて計算する。また時間と速度の離散化も行い、離散化された時間をステップと呼ぶ。粒子は1ステップで格子線に沿って別の格子点へちょうど到達する速度で移動する。
LBMでは空間の次元数と静止を含め、向きを考慮した速度の数をそれぞれ$n$と$m$として$n$D$m$Vと言うように表す。例えば、2次元で9つの速度を持つ場合2D9Vとなり下図の様に表すことが出来る。
ここで数字は速度を離散化した時の指標である。
LBMは衝突と並進を表す二つの分布関数を計算する事で粒子分布を求める。 衝突後の粒子分布を$\overline{f_a}(t,\boldsymbol{r}) $、衝突前の粒子分布を$f_a(t,\boldsymbol{r})$とすると、衝突を表す式は(\ref{collision_step})となる。
ここで$a$は粒子の持つ速度の方向、$\boldsymbol{r}$ は位置ベクトル、$\Omega_a[f_a(t,\boldsymbol{r})]$ は衝突による粒子分布の変化を表し$\Omega_a$は衝突演算子と呼ばれる。
続いて並進は$\tau$ を時間刻み、$\boldsymbol{c}_a$を$a$方向の並進速度ベクトルとすると式(\ref{streaming_step})となる。
式(\ref{collision_step})と式(\ref{streaming_step})を組み合わせる事によって、衝突と並進による状態変化を考慮した式(\ref{lattice_boltzmann})が求まる。この式を格子Boltzmann方程式と言う。
式(\ref{lattice_boltzmann})の右辺第二項は衝突項と呼ばれる。衝突項の計算方法に本研究では衝突による粒子分布の変化量が一定の割合で減少して平衡状態に近づいていくとするBGKモデルを用いて衝突項を式(\ref{BGK_model})の様に置き換えた。
式(\ref{lattice_boltzmann})と式(\ref{BGK_model})を組み合わせると式(\ref{lattice_BGK_model})が求まる。
式(\ref{lattice_BGK_model})は格子ボルツマン方程式における格子BGKモデルと呼ばれる。ここで$f^{(0)}_a(t,\boldsymbol{r})$は局所平衡分布関数と呼ばれるもので、次のような式である。
ここで$\boldsymbol{e}_aと$$w_a$は2D9Vの場合、
a | $\boldsymbol{e}_a$ | $w_a$ |
0 | (0,0) | 4/9 |
1,2,3,4 | (1,0),(0,1),(-1,0),(0,-1) | 1/9 |
5,6,7,8 | (1,1),(-1,1),(-1,-1),(1,-1) | 1/36 |
となる。
一方、各格子点の密度$\rho$と流速$\boldsymbol{u}$は次式から求められる。
ここで$c$は移流速度として$c=\delta x/\delta t$で定義される。$\delta x$、$\delta t$はそれぞれ空間刻み、時間刻みを表す定数である。
式(\ref{lattice_BGK_model})をある格子点の分布関数を求めるよう変形すると
となる。プログラムでは式(\ref{lattice_BGK_model_2})の方を用い、時刻$t$の状態から時刻$t+\tau$の格子点の分布関数を求めている。
本研究では図cavity_flow_simの様な正方キャビティ流れと呼ばれる物理現象をシミュレーションするプログラムを出力する。本章では正方キャビティ流れの説明と共に、シミュレーションに使用した境界条件についても説明する。
図cavity_flow{の様な正方形形状の領域を考え、3辺(左右、下部)は固定された固定壁、残る一つ(上部)が一定速度で動く移動壁とする。この時正方形領域内の流体は移動壁に引きずられて動き、右側の壁に衝突して下方向に曲げられる。曲げられた流体はそのまま進み、やがては下部の壁にぶつかり左へ曲げられる。他方、上部左側では流体が右へ移動する為、その不足分を補うために下部から流体が流れ込む。このようにして領域内では最終的に流体が循環するような流れが発生する。この流れの事をキャビティ流れと呼ぶ。
本研究で作成したプログラムはこのキャビティ内流れを計算するカーネルを出力する。
CFDではシミュレーション領域で壁などの流体の運動に影響を与えるものが存在する事がある。その場合は流体の方程式だけでは解を決める事は出来ない為、補助的な条件を設定する事により解を求める。この補助的な条件の事を境界条件と言う。
LBMは式(\ref{lattice_BGK_model_2})より、ある格子点の計算に周囲の格子点の値を利用す。従って、壁面に接する格子点では壁面から流れ込む粒子の分布関数は式(\ref{lattice_BGK_model_2})では求めることはできない。そこでBounce-Back条件を用いて近似する。
Bounce-Back境界条件は粒子を侵入方向から$180^\circ$跳ね返すもので、8番目の方向に進む粒子の場合図bounce_back_graphの様になる。
本研究では図cavity_flowの左右、下側の三方向の壁面にBounce-Back境界条件を適用している。
ディリクレ境界条件とは格子に直接値を与える境界条件である。正方キャビティ流れでは図cavity_clow_2の移動壁を再現する為に領域上部において右向きに一定の流速を与えている。
本研究では一つのカーネルに掛かる計算時間を短縮させる為にカーネル内でのメモリアクセスが少なくなるよう調整したカーネルプログラムを出力するプログラムを作成した。
なお、使用した言語はC#で出力されるカーネルプログラムは2次元の正方キャビティ流れをLBMで計算するプログラムである。
本プログラムは次の手順でカーネルプログラムを作成する。
CUDAによる高速化を行う前に、基本的なCFDを行うCPUコードの説明を行う。プログラムの流れ図を図cpu_flowに示す。CPUコードはCUDAで並列に計算していた格子毎の計算を全ての格子に対しループを用いて計算している。また、CUDAでは境界条件による値の選択をif文無しに行っていた。これはGPGPUがif文などの条件分岐の処理に対し最適化されておらず、一般に条件分岐は計算速度を損なわせるからである。これに対しCPU側ではそのような制限は無いので格子の種類による計算処理の方向を条件分岐によって決定している。
CUDAは非常に多数のスレッドを用いて並列計算を行う事が出来る。CFDでは一般に空間を離散化し、その離散点一つ一つで方程式の計算を行う事により全体の結果を導き出す。よってCUDAでのCFDの計算では、スレッド一つを離散点一つに対応させ全離散点を並列計算する事で高速化を図る。
しかし、CUDAは使用するGPUによって生成できるスレッド数やブロック数が制限される。その為生成できる総スレッド数が総計算点よりも少ない場合が発生する事を考慮し、一つのスレッドで複数の計算点の計算を行うようカーネルを作成する必要がある。
図base_programはCFDの基本的なプログラムを図示したものである。
計算に使用するデータは現在の状態($t$ステップ)を保持する物(以降、データA)と計算後($t+1$ステップ)の値を格納する物(以降、データB)の二つを用意する。
CUDA側では大量に生成されたスレッドがデータAを用いて$t+1$ステップの状態を計算し、その結果をデータBに保存する。その際、スレッドは複数の格子点をループを用いて計算する。CPU側ではCUDAに計算命令を出した後、計算が終了するまで待機する。計算が終了したらデータAとデータBのポインタを入れ替えて再びCUDAに計算命令を出す。データAとデータBのポインタを入れ替える事でCUDAは$t+1$ステップのデータを用いて状態を計算し始める。その為、結果的に$t+2$ステップの状態を計算できる。この様にデータのポインタを入れ替える事でステップを進めている。
データのやり取りはグローバルメモリを介して行われる。また気体定数等の定数はコンスタントメモリにCUDA初期化時に転送する。
出力されるカーネルで使用するデータは一次元配列で管理し、引数として渡される。データは更新前と更新後の二つを用意する。カーネルの引数は格子点の密度、速度、分布関数値を格納するdouble型のポインタdensityとn_densityと、流体セルや壁、流入や流出境界を示すint型のポインタmaskとn_maskを持つ。先頭にn_ を持つ引数は次ステップ用の配列である。
密度、速度、分布関数値を格納する方の一次元配列は、例えば2D9Vの環境で格子が図latticeのように$3\times3$の格子で左上から右下へ0~8までの格子番号が振られているとすると、データの並びは図memoryように先頭に格子番号が0~8番までの密度の値が、その後ろに同じく0~8番までのX方向の風速が並ぶ、このように格子番号順にY方向の風速、Z方向の風速、速度方向0~8番の分布関数値が後ろに続く。
通常シミュレートする空間には図lattice_bgk_modelの様に流体の流入口や流出口、壁などが存在する。これらには式(\ref{lattice_BGK_model})を適用できないのでカーネル内で格子の種類により値を変化させる必要がある。そこで格子の種類を整数値の識別番号を用いて表現する事でカーネル内で識別できるようにする。カーネルに渡される格子の種類を格納した配列は図array_mappingの様に空間の左上から右下へマッピングされた1次元配列として渡される。
LBMでは注目している格子の値を周りの格子の値を利用して計算する為、領域の端においては計算を行う事が出来ない。そこで計算領域との境界に境界条件を付加する事で計算を可能にする。
本研究では条件分岐を行う事による計算速度の低下を防ぐため、図boundaryの様に計算領域外との境界をあらかじめ格子として用意し、計算を行うのはその内側(図boundaryにおける青い格子)のみにする事で参照する格子点が領域外かそうでないかの判定を行わずに済むようにしている。
カーネルプログラムの流れを図program_flowに示す。
カーネルプログラム内ではループを用いて1スレッド多格子点の計算を実現している。ループ内は1つの格子点の計算を行う。まずループ回数を元に計算する格子点のデータ位置を算出する。次に全方向に対する式(\ref{lattice_BGK_model})、式(\ref{peq})の計算を逐次行う。この時計算格子点の周りの格子点の値を用いて分布関数値を計算するが、その周りの格子点が壁や流入境界、流出境界だった場合を想定して値の選択を行う。値の選択はListingno_branchの様に条件分岐を使わずに行われる。
結果= (mask == NORMAL) * 分布関数値;
結果+= (mask == BOUNDARY) * 壁(bounc-back)境界の値;
結果+= (mask == INFLOW) * 流入境界の値;
結果+= (mask == OUTFLOW) * 流出境界の値;
ここでNORMAL、BOUNDARY、INFLOW、OUTFLOWはC言語のマスクで、それぞれ流体セル、壁、流入境界、流出境界を表す識別番号に置き換えられる。maskは現在計算に使用している格子点の種類を示す整数型の識別番号で前述の4つのマスクの値のいずれかを持つ。
Listingno_branch_listingはC言語の関係演算子(ここでは同値を意味する==演算子)が真では1を、偽では0を返す事を利用して値の選択を行っている。つまり関係演算子の結果と各条件での値を乗算し、全体を加算する事で関係演算子が真の値のみが結果に保存される。
全方向の分布関数が計算し終えると式(\ref{density})、式(\ref{velocity})を計算し密度と速度を算出、次ステップのデータに保存する。
CUDAではループ回数が異なるスレッドがWarp内に存在する場合は全スレッドで最大のループ回数に合わせてループが実行される。よってWarp内の全てのスレッドが同じ回数ループするのならば問題はないが、異なる回数を実行する場合は図warp_kinituka_の様に無駄なリソースを使うことになる。この場合、Warp内のスレッドが行うループ数を均一化する事でリソースの空きをなくすことが出来る。CFDでは生成可能なスレッド数を分割した格子点数が上回る場合を想定して、1スレッドで多格子点を計算できるようにしている。このためにはカーネル内でループを行うが、格子点数によってはスレッドによってループ回数が異なってくる場合がある。本研究ではこれを避けるために全てのスレッドで同じ回数ループするカーネル(\roop )と、ループをしないカーネル(\noroop )、余ったスレッドを実行するカーネル(\rem )の最大三つに分割する。分割に際しては、計算格子点数($l$)、上限ブロック数($b_{max}$)、ブロック当たりの上限スレッド数($t_{max}$)を元に実行ブロック数、ブロック当たりの実行スレッド数、ループ回数を計算する。
ループ回数($r_n$)は式(\ref{roop_calc})で決定される。
下付き文字のnは整数で0の時\roop 、1の時\noroop 、2の時\rem のパラメータを表す。
なお$b_{max}$と$t_{max}$はGPUの制限を超えない範囲の任意の整数であり、GPUの能力を最大限生かすためには$t_{max}$は32の倍数である事が望ましい。
実行ブロック数($b_n$)とブロック当たりの実行スレッド数($t_n$)は式(\ref{block_thread_calc})、式(\ref{thread_calc})で決定される。
カーネル毎の格子データ配列のオフセット($offset_n$)は式(\ref{offset_calc})で決定される。
各カーネルのパラメータが計算し終わると、パラメータを元にカーネル内の定数を展開する。主に展開するのは配列のインデックス値や方向ベクトルの値である。
Listingnoroop_source、deploy_sourceに本研究で作成した生成プログラムによって出力された定数の展開前と展開後のカーネルコードの一部を示す。
index_a = index_out - ex[0] - c_data.x_max_out * ey[0] - c_data.x_max_out * c_data.y_max_out * ez[0];
_vx += ex[0] * c_data.c * pa * boundary;
_vy += ey[0] * c_data.c * pa * boundary;
_vz += ez[0] * c_data.c * pa * boundary;
index_a = index_out - (0) - (256) * (0) - (65536) * (0);
_vx += 0 * c_data.c * pa * boundary;
_vy += 0 * c_data.c * pa * boundary;
_vz += 0 * c_data.c * pa * boundary;
ここで展開前のc_dataはコンスタントメモリに格納された格子点の情報を持つ構造体である。またex、ey、ezは方向ベクトルの各要素の配列で、c_data同様コンスタントメモリに格納されている。
このように格子点数等の情報をカーネルに埋め込むことで、コンパイラの最適化を促しさらにメモリアクセスを減らすことで高速化を図る。
格子点数が256×256で使用するブロックとスレッド数を128×128、速度数を9にした時の出力されるカーネルを付録のListingkernel_no_deployment、kernel_deploymentに示す。出力されるプログラムの説明を次に示す。
測定環境を表environmentに、測定条件は表measurement_conditionsに示す。
OS | Windows10 64bit |
CPU | Intel Core i7-4820K CPU 3.70GHz |
GPU | NVIDIA GeForce GTX TITAN |
CUDA | Ver 7.5 |
RAM | 32GB |
次元数 | 2次元 |
速度方向数 | 9 |
計測ソフト | NVIDIA Visual Profiler Veer7.5 |
計測は$256\times256$、$512\times512$、$1024\times1024$格子に分割した空間をそれぞれ1000、10000、100000ステップ計算した。またそれぞれの計測は3回行い平均値を結果とする。表中の展開の列は、有りの項目は\deploykernel を、無しの項目は\nodeploykernel の実行結果を意味する。また$256\times256$格子、1000、10000ステップの計算をOpenMPを使用したCPUコードで行った。
それぞれの計算時間の結果を図result_grafuに示す。またCUDAコードにおける$256\times256$格子、1000ステップ計算時のスレッド当たりのカーネル数を図register_threadにOcuppancyの値を図occupancy、グローバルメモリの読み込み回数と書き込み回数をそれぞれ図GLT、図GSTに示す。
図result_grafuから、\deploykernel の方が\nodeploykernel よりも約21%高速に動作する事が確認できた。
また図register_thread、から、定数の展開を行った場合は行わなかった場合に比べ全体的にレジスタの数が減少している。このレジスタ数の減少はカーネルコードにコンスタントメモリに記録されていた値を展開したことによって、コンスタントメモリの値を格納するためのレジスタが削減できたためであると考える。つまりListingnodeploy_sourceにおいてex、ey、ez配列やc_data構造体から値をいったんレジスタに退避していた所を、Listingdeploy_sourceの様に値を直接展開したことによってレジスタに退避する必要がなくなったという事である。
図occupancyが示すOccupancyの向上は、カーネルに必要とされるレジスタ数が減少したことによりSMXが一度に実行できるWarp数が増加した事が原因であると考える。
図GLT、図GSTから展開有りの方が無い場合に比べ読み込み回数が減少している一方、書き込み回数には変化がない。コンスタントメモリはカーネルからは読み込みは出来ても書き込みが出来ない事を考えると、コンスタントメモリの値を展開したことによってコンスタントメモリアクセスが減った事がこれらの数値に影響を与えていると考えられる。またCUDAではOff Chipのメモリアクセスは演算に比べて非常に低速である事から、メモリアクセス回数が減った事が実行時間の削減に寄与していると考えられる。
以上より、カーネルの実行速度が向上したのにはコンスタントメモリを利用していた定数値をカーネル内に展開する事でメモリアクセスの回数が削減されたこと、レジスタ数が減少したことによりSMXで一度に実行できるWarp数が増加したことが原因であると考えられる。
図result_grafuからCPUコードに比べCUDAコードの実行速度はCUDAコードの高速化を施していない場合であっても約100% の高速化が図れていることが分かる。この事から、CFDの計算をCUDAに行わせるだけでも非常に高速に動作する事が分かった。
本研究ではLBMを用いた流体シミュレータをプログラムで出力する事により、定数をプログラム内に埋め込む事で計算の高速化を図った。結果として約21%の高速化を図ることが出来、事前計算による定数の埋め込みはCUDAの更なる高速化に対して有効であると考える。
本研究では初めにLBMとCUDAを利用した流体シミュレーションカーネルを開発し、その後開発したカーネルコードを元に定数を埋め込むジェネレートプログラムを作成した。このジェネレートプログラムは21%高速化したカーネルを出力できるが、カーネルの内容をプログラム内に埋め込んでいる為非常に汎用性に乏しいプログラムとなってしまった。
今後の課題として、よりカーネルの自由度を高めるためにCUDAのカーネルコードを読み込みその内容を解析して動的に定数を埋め込んだり、あるいは式の事前計算や0との乗算と言った不要な計算を省けるような機構を持つシステムの開発を進めていきたい。
LBMの局所平衡分布感化数(式(\ref{peq}))の係数wの値を文字列として保持するクラスである。
方向ベクトルを保持するクラスである。内部はVector3<double>型の配列を持ち、そのサイズは速度方向数分となる。
Bounce-Back境界条件用の速度番号の反転テーブルを保持するクラスである。内部ではdouble型の配列を持ち、サイズは速度方向数分となる。値にはインデックスの番号と反対方向の速度番号が入っている。
このクラスは生成されるLBMの計算カーネル(roop_kernel、noroop_kernel、remaining_kernelカーネル)に関する情報を保持するクラスである。
メンバ変数について説明する。
メソッドについて説明する
このクラスは格子点数やLBMの速度の数、使用できる最大ブロック数およびブロック当たりの最大スレッド数等の事前計算に必要な情報を保持するクラスである。
メンバ変数を説明する。
メソッドについて説明する。
HostGeneratorはCPU側で動作するコード(main.cuファイル)を出力するクラスである。LBMCodeGeneratorを継承する。
HostGeneratorはプログラムのエントリポイントとなるmain関数、速度のデータをファイルに書き込むwriteVelocityData関数、圧力のデータをファイルに書き込むwritePressData関数を出力する。出力はオーバーライドしたgenerate(LBMConfig)メソッドを呼び出すことで行われる。出力中にエラーが発生するとfalseが、完了するとtrueが帰る。
ConfigGeneratorクラスはプログラムで使用するマクロを定義するlbm_config.cuhファイルを出力するクラスである。generateメソッドの引数にLBMConfigクラスのインスタンスを与える事で格子点数やデータのオフセット値等の定数をマクロとして書き出す。出力はオーバーライドしたgenerate(LBMConfig)メソッドを呼び出すことで行われる。出力中にエラーが発生するとfalseが、完了するとtrueが帰る。
DataGeneratorクラスはプログラムで使用する構造体等を定義するlbm_data.cuhファイルを出力するクラスである。生成される構造体は次の通りである。
出力はオーバーライドしたgenerate(LBMConfig)メソッドを呼び出すことで行われる。出力中にエラーが発生するとfalseが、完了するとtrueが帰る。
LBMCodeGeneratorクラスはコードを生成しファイルに出力するクラスである。generateメソッドの引数にLBMConfigクラスのインスタンスを渡す事で、LBMConfigクラスの内容に基づいたカーネルの分割、定数の展開を行いファイルとして出力する。戻り値はbool型で生成中にエラーが発生するとfalseが、生成が完了するとtrueが帰る。
メンバ変数としてKernelInfoクラスのリストを保持する。
メソッドについて説明する。
KernelGeneratorDeploymentクラスはlbm_kernel_deployment.cuhファイルを出しゅつりょ力するクラスである。このクラスはgenerate関数にLBMConfigのインスタンスを引数に与える事で、格子点数や方向ベクトルの値などをソースコードに展開してファイルを出力する。コードの生成の際にはコードの一部分の生成を担当するメソッドを使用する。これらのメソッドは自分の担当するコードを戻り値として返すため、コード生成はこれらメソッドの戻り値を合成し一つのコードを作り上げる。
このクラスの持つメンバについて説明する。
フィールドについて説明する。
このクラスはlbmkernelnodeployment.cuhファイルを出力するクラスである。このクラスはKernelGeneratorDeploymentクラスを継承していて、一部のメソッドをオーバーライドしている。
オーバーライドしているメソッドの変更部分について説明する。
このファイルにはデータの書き込み用関数とエントリポイントとなるmain関数が含まれる。
main.cuの5行目のinclude文は参照するファイルをlbm_kernel_deployment.cuhにすると最適化されたカーネルを、lbm_kernel_nodeployment.cuhにすると最適化されていないカーネルを使用して計算する。
main関数はコマンドライン引数として次の値を取得する。
main関数は初めにpreparation関数を実行して定数の初期化を行う。
次にinitialize関数を実行しLBMPacage構造体を初期化して、データのポインタを取得する。
初期化が終わると計算を開始する。ループを用いてコマンドライン引数で指定された終了ステップ数まで処理を続ける。
CUDAに計算の実行を指示するのはexecution関数でこの関数が呼ばれるとCUDAは1ステップの計算を開始する。計算が終わるまでブロッキングされる。
計算が終わるとall_exchange関数を呼び出してLBMPackage内のポインタを入れ替えてステップを進める。
もし、現在のステップ数が書き込み開始ステップ数以上で、書き込み間隔との剰余が0ならばデータのファイル書き込みを行う。
その場合getResultData関数を用いてデータを取得する。データはLBMPackage構造体のh_resultメンバに格納される。
LBMPackage構造体のh_resultメンバをwriteVelocityData関数及びwritePressData関数に与えてファイルの書き込みを行う。
ステップが終了ステップまで到達すると、allCudaFree関数を実行し、CUDAの為に生成したメモリなどの解放を行う。
最後にcudaDeviceReset関数を実行しCUDAを完全終了させる。
なお、前述に出てきたpreparation関数、initialize関数、execution関数、all_exchange関数、getResultData関数、allCudaFree関数はいずれも5行目のinclude文で指定したファイル(lbm_kernel_deployment.cuhかlbm_kernel_nodeployment.cuh)の中で定義された関数である。
このファイルにはプログラム内で使用するマクロが定義されている。マクロの説明を行う。
このファイルにはプログラム内で使用する構造体が定義されている。
このファイルには最適化を施していないカーネルコードが定義されている。
グローバル変数について説明する。なお先頭に__constant__が付く変数はCUDAのコンスタントメモリに置かれる変数である。
各関数について説明する。なお、先頭に__global__ 修飾子が付く関数はGPUで実行できるカーネルプログラムである。
このファイルには最適化を施したカーネルコードが定義されている。
lbm_kernel_nodeployment.cuhとの違いは、roop_kernel、noroop_kernel、remaining_kernelカーネル内において動的生成によって方向ベクトルや格子数などの定数がソースコード上に展開されている点である。その他のinitKernel、lbmResultKernel、exchange、all_exchange、mallocData、preparation、initialize、getResultData、execution、allCudaFree関数はすべてlbm_kernel_nodeployment.cuhと同じ処理を行う。