GPUに処理をさせる点で一番必要なものは前章までで説明してきたメモリ関連の話でした。
どこに確保されているのかをしっかりと意識してデータを扱うことが重要です。
この章ではもっと本格的に計算を始めるために、こまごましたデータをGPUに送る方法を示します。例えば以下の例、
#include <amp.h>
#include <iostream>
using namespace concurrency;
void test()
{
accelerator Acs;
array<float , 1> vAGpu(100 , Acs.get_default_view() );
array_view<float , 1> vAGpuView();
vector<float> vACpu();
float Δt;
Δt = 0.2;
vAGpuView = vAGpu;
parallel_for_each(
Acs.get_default_view() ,
AEx ,
[=](index<2> indC) restrict(amp)
{
float x;
x = indC[0];
vAGpuView[indC] = x*Δt;
}
);
vACpu = pvAGpu[0];
for(int ic=0;ic<AEx[0];ic++)
{
for(int jc=0;jc<AEx[1];jc++)
{
cout << vACpu[ic*AEx[0] + jc] << " ";
}
cout << "\n";
}
}
この色で囲まれた部分がGPUで実行されるコードになります。ご覧のようにCPU側のメモリに配置されるであろうΔtなる変数を、GPU上で実行されるコードでそのまま利用できています。これくらいのこまごました変数ならば、each_for_parallelを呼び出し、GPUへ処理を渡す時に一緒に転送してくれます。
なのでユーザーはパラメータなどのデータ変数に関する転送を気に掛ける必要がありません。
しかし、この便利な機能にはいくつもの制限があります。以下に列挙します。
GPUへ渡せる変数の制限
- 値渡しで転送される
- ローカル変数のみ
- ポインタは不可
- 関数は渡せない
等ですかね。特にローカル変数しか渡せず、ポインタは渡せないという点が大きな障害となります。
まず、グローバル変数などをGPUへ渡したい場合、一度each_for_parallelが呼び出される関数内のローカル変数へ代入してからになります。
そしてポインタを渡す場合、例えば配列で渡したい場合などですね。これはあきらめるしかありません。あまりにも大きい配列であるならばそもそもarrayオブジェクトでデータを参照すべきです。3つ程度の配列であるならば、以下の記述をお勧めします。
accelerator Acs;
array<float , 1> vAGpu(100 , Acs.get_default_view() );
array_view<float , 1> vAGpuView();
float a1,a2,a3;
float vA[3] = {2,2,1};
vAGpuView = vAGpu;
a1 = vA[0];
a2 = vA[1];
a3 = vA[2];
parallel_for_each(
Acs.get_default_view() ,
AEx ,
[=](index<2> indC) restrict(amp)
{
float x;
float vAa[3] = {a1,a2,a3};
x = indC[0];
vAGpuView[indC] = x*Δt*vAa[2]+vAa[0]+vAa[1];
}
);
GPUのコード内で配列を組立直すのですが・・・ほかに良い記述が思いつきませんでした。
なお、配列の再構築において、以下のようなコードも組めます。
accelerator Acs;
array<float , 1> vAGpu(100 , Acs.get_default_view() );
array<float , 1> vBGpu(100 , Acs.get_default_view() );
array<float , 1> vCGpu(100 , Acs.get_default_view() );
array_view<float , 1> vAGpuView();
array_view<float , 1> vBGpuView();
array_view<float , 1> vCGpuView();
float a1,a2,a3;
float vA[3] = {2,2,1};
vAGpuView = vAGpu;
vBGpuView = vBGpu;
vCGpuView = vCGpu;
a1 = vA[0];
a2 = vA[1];
a3 = vA[2];
parallel_for_each(
Acs.get_default_view() ,
AEx ,
[=](index<2> indC) restrict(amp)
{
float x;
float vAa[3] = {a1,a2,a3};
array_view<float , 1> vAView[3] = {vAGpuView , vBGpuView , vCGpuView};
x = indC[0];
inr c;
while(c<3)
{
vAView[c][indC] = vAView[c][indC]*Δt + vAa[c];
c++;
}
}
);
上記記述は便利なんじゃないでしょうかね?
私はこれを思いつくまで大変苦労しました。
ラムダ式
GPUに処理を渡すとき、parallel_for_each()関数に指定している、
[=](index<2> indC) restrict(amp){}
なる記述ですが、これはCの関数と同じものだといえます。C++でラムダ式といわれる機能で、処理の内容を引数として関数に渡せる機能です。
GPUに実行させるコードは、対CPU用にコンパイルされるC言語の関数では互換性がありません。よって処理の内容を渡すという意味で、このラムダ式の部分だけGPUようにコンパイルするのだと思います。しかし、その実態はC言語でいう関数みたいなものであり、上記のようにローカル変数を宣言できたりします。
Cの関数と異なる点といえば、自身が記述されている関数内のローカル変数を参照できるということがあげられます。よってΔtが参照できます。
[=]
という記述は、ラムダ式の外の変数をどういった方法で参照するかといった動作定義になります。= は値渡しで、ラムダ式内でΔtの値を変えようとしても、もとの変数の値は変わりません。GPUにラムダ式を渡す場合、この[=]しか使わないと思ってもらってよいと思います。というかほぼこれしか指定できません。
GPUからΔtの値を変えるなんて、ケチなことは言いませんので。