仮眠プログラマーのつぶやき

自分がプログラムやっていて、思いついたことをつぶやいていきます。

自作ゲームやツール、ソースなどを公開しております。
①ポンコツ自動車シュライシュラー
DOWNLOAD
②流体力学ソース付き
汚いほうDOWNLOAD
綺麗なほうDOWNLOAD
③ミニスタヲズ
DOWNLOAD
④地下鉄でGO
DOWNLOAD
⑤ババドン
DOWNLOAD
⑥HLSLサンプル
DOWNLOAD
⑦圧縮拳(ツール)
DOWNLOAD
⑧複写拳
DOWNLOAD
⑨布シミュレーション
DOWNLOAD
⑩hspでgpgpu
DOWNLOAD
⑪Handbrakeドラッグドロップツール
DOWNLOAD
⑫minecraft巨大電卓地形データ
DOWNLOAD
⑬フリュードランダー
デジゲー博頒布α版
DOWNLOAD
⑭パズドラルート解析GPGPU版
DOWNLOAD
⑮ゲーム「流体de月面着陸」
DOWNLOAD

テザリングができない。DNSサーバーに問題がある可能性があります

スマホでテザリングしたがどうもネットに繋がらない、で若干ハマったのでメモ

機種はXiaomi mi 10 Global版、Simはmineo Dプラン
それまでxperia compact XZ1でテザリングできていたのでsimに問題がある可能性は低いと思った。
ひとまずどういう現象かというと「DNSサーバーに問題がある可能性があります」というメッセージがトラブルシューティングで発生している。

mudai

じゃあDNSを設定すればよいと思ってPCのネットワーク設定でDNSを手動で8.8.8.8とかに設定したがダメ。そもそもIPアドレス直打ちでやってもアクセスできなかったのでDNSの問題ではないと思った。
この「DNSサーバーに問題がある可能性があります」でググってもなかなか情報が出てこなかったので半ば諦めていたが、どうもXiaomi機種特有の問題のようだった。
https://2week.net/17429/

こちらを参考にNVMO値をSPNにすることで嘘のように解決した。
sukusyo


めでたしめでたし

UnityでGPGPUその4 ComputeShaderでAtomic演算

GPUプログラミング(CUDAとかOpenCL)で、GPU上にある配列の総和を求めたいということは良くある。逐次処理なら簡単だが並列処理ではそうはいかない。
前回shared memoryを使った配列の総和を計算したがこれは良い面も悪い面もある。良い面としては高速なこと、悪い面はややプログラムが煩雑になること。1SM内でしかデータを共有できないため1024要素程度までなら良いが1000万要素もの配列となるとSM内に入りきらない。

そこで他の方法を考えるが、GPU全コアが共有してアクセスできるのはglobal memory(GDDR5とか)しかなく、同時並列に1つのアドレスに加算処理すると正しい結果が得られない。詳しくはデータレースで検索。
普通にやるとダメなので、変数(アドレス)に対する同時アクセスをスレッド間で排他制御する必要がある。それを叶えてくれるのがAtomic演算!


CUDAでもOpenCLでもAtomic演算はできるし、裏を返せばハードウェア的に、NVIDIA、AMDのGPUでもAtomic演算ができる機能は備わっているはず。どうせ使える機能ならUnityでも使いたい。

例のごとく空のオブジェクトとHost.csとComputeShader.computeを生成して
C#(Host.cs)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class Host : MonoBehaviour
{
    public ComputeShader shader;
    void Start()
    {
        uint N = 1024;//256の倍数で
        uint[] host_B = { 0 };//初期値
        ComputeBuffer AtomicBUF = new ComputeBuffer(host_B.Length, sizeof(uint));
        int k = shader.FindKernel("sharedmem_samp1");
        AtomicBUF.SetData(host_B);

        //時間測定開始
        int time = Gettime();

        //引数をセット
        shader.SetBuffer(k, "atmicBUF", AtomicBUF);

        //初回カーネル起動
        Debug.Log("初回カーネル起動前" + (Gettime() - time));
        shader.Dispatch(k, (int)N/256, 1, 1);
        Debug.Log("初回カーネルDispatch後  " + (Gettime() - time));
        AtomicBUF.GetData(host_B);
        Debug.Log("初回カーネルGetData直後  " + (Gettime() - time));

        // こっちが本命。GPUで計算
        host_B[0] = 0;
        AtomicBUF.SetData(host_B);
        shader.Dispatch(k, (int)N/256, 1, 1);
        Debug.Log("本命カーネルDispatch直後 " + (Gettime() - time));

        // device to host
        AtomicBUF.GetData(host_B);

        //結果
        Debug.Log("本命カーネル確定終了    " + (Gettime() - time));
        Debug.Log("\nAtomic_Addの結果   "+host_B[0]);

        //解放
        AtomicBUF.Release();
    }

    // Update is called once per frame
    void Update()
    {
    }

    //現在の時刻をms単位で取得
    int Gettime()
    {
        return DateTime.Now.Millisecond + DateTime.Now.Second * 1000
         + DateTime.Now.Minute * 60 * 1000 + DateTime.Now.Hour * 60 * 60 * 1000;
    }
}

ComputeShader(ComputeShader.compute)
#pragma kernel sharedmem_samp1
RWStructuredBuffer<uint> atmicBUF;//1要素

[numthreads(256, 1, 1)]
void sharedmem_samp1(uint id : SV_DispatchThreadID) {
	InterlockedAdd(atmicBUF[0], id);
}

atomicadd

https://github.com/toropippi/Uinty_GPGPU_SharedMem_AtomicAdd/tree/master/gpgpu_sample4

0~N-1までの総和を求めることができた。
(計算時間を測定するのがくせなので初回カーネルとか書いてあるが今回はあまり重要ではない)

ただ基本的にはAtomic演算は遅いと思っていたほうが良い。グローバルメモリに対してGPU全コアから排他制御でアクセスしているので。
なので全GPUコアからAtomic演算をするのではなく、前回の記事のようにshared memory使ったやつで、1SM内で総和を求めた後、その値をAtomic演算で総和していくのがプログラムを書く上でも簡単で良いかなと私は考えている。

また、今回のはAtomic演算のatomic_addを使ったサンプルになる。ほかにも引き算やビット演算などAtomic演算はいくつもあるので適宜調べてほしい。

UnityでGPGPUその3 ComputeShaderの共有メモリ(shared memory)

【GPUの共有メモリを使う】
今回は256要素の配列の総和を求める計算をする。リダクション(Reduction)ともいう。
こちらがその簡素なコード
C#(Host.cs)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Host : MonoBehaviour
{
    public ComputeShader shader;
    void Start()
    {
        float[] host_A = new float[256];
        for(int i = 0; i < 256; i++)
        {
            host_A[i] = 1f * i;
        }
        float[] host_B = { 0f };//この数字はなんでもいい。Bは結果がはいる側

        ComputeBuffer A = new ComputeBuffer(host_A.Length, sizeof(float));
        ComputeBuffer B = new ComputeBuffer(host_B.Length, sizeof(float));

        int k = shader.FindKernel("sharedmem_samp0");

        // host to device
        A.SetData(host_A);

        //引数をセット
        shader.SetBuffer(k, "A", A);
        shader.SetBuffer(k, "B", B);

        // GPUで計算
        shader.Dispatch(k, 1, 1, 1);//ここでは1*1*1並列を指定。ComputeShader側で256並列を指定している

        // device to host
        B.GetData(host_B);
        Debug.Log(host_B[0]);

        //解放
        A.Release();
        B.Release();
    }

    // Update is called once per frame
    void Update()
    {
    }
}

ComputeShader

#pragma kernel sharedmem_samp0
RWStructuredBuffer<float> A;//256要素
RWStructuredBuffer<float> B;//1要素

groupshared float block[256];
[numthreads(256, 1, 1)]
void sharedmem_samp0(int id : SV_DispatchThreadID, int grid : SV_GroupID, int gi : SV_GroupIndex) {
	float a = A[id];
	block[gi] = a;
	//共有メモリに書き込まれた
	GroupMemoryBarrierWithGroupSync();
	for (int i = 128; i > 0; i /= 2) {
		if (gi < i) {//128,64,32,16,8,4,2,1
			block[gi] += block[gi + i];
		}
		GroupMemoryBarrierWithGroupSync();//ここは256スレッドすべてで実行
	}

	//あとは0番目のデータを抽出するだけ
	if (gi==0)
		B[0] = block[0];
} 
https://github.com/toropippi/Uinty_GPGPU_SharedMem_AtomicAdd/tree/master/gpgpu_sample3

前の記事のように
Assetsフォルダ内にScriptsフォルダを作成して以下の2つを作成
・右クリック→Create→C# script→「Host」
・右クリック→Create→Shader→Compute Shader→「ComputeShader」
空のobjectにC#をアタッチし、そのオブジェクトの「shader」に「ComputeShader」をアタッチ
そして実行
結果

samp3結果0






解説するとまずC#側でhost_Aという配列変数に0,1,2,3,4・・・255の数値を格納しGPUに転送。GPU側で全要素を並列に足し算しC#側に結果を戻し表示している、という感じ。
検算してみると
0,1,2,3・・・なのでN*(N-1)/2なので256*255/2=32640。
あってる。



今回初見の命令は全部ComputeShader側コードで
・groupshared float block[256];
・int grid : SV_GroupID, int gi : SV_GroupIndex
・GroupMemoryBarrierWithGroupSync();

の3つ。
まずgroupshared float block[256];
これはカーネルで共有メモリを使いたいときに宣言する。float型×256要素を確保している。共有メモリとはうまく使うことでいろいろ高速化できる優れもの。で以下の特徴がある
・グループ内(この場合256スレッド)でしか内容が共有されない
・他グループからはアクセス不能
・そのカーネル実行中にのみアクセスできる
・カーネル終了後はアクセスできない
・カーネル終了後は自動で消えるので解放処理はいらない
・実体はL1キャッシュに存在する
・L1なのでグローバルメモリよりはるか高速に(10倍とか)アクセスできる


次にint grid : SV_GroupID, int gi : SV_GroupIndex
void sharedmem_samp0(int id : SV_DispatchThreadID, int grid : SV_GroupID, int gi : SV_GroupIndex) {
この行の後ろ2つのやつだ。
SV_DispatchThreadIDは前もでてきたようにスレッドidであり、上のコードにならってみるとカーネルが256並列なので0~255の値が各スレッドで割り当てられている

SV_GroupIDはグループidで、カーネル256並列を1グループ*256スレッドという形で実行しているのでグループは一つしかないため、このグループidには0が割り当てられることになる。
この場合の1と256は
C#の
shader.Dispatch(k, 1, 1, 1)
とComputeShaderの
[numthreads(256, 1, 1)]
に対応している。
C#ではグループ数の指定、ComputeShaderカーネルでは1グループあたりのスレッド数の指定をしていたというわけだ。
SV_GroupIndexはグループ内のスレッドindexで0~255の値が割り当てられることになる。


最後にGroupMemoryBarrierWithGroupSync();
これはグループ内で全スレッドの同期をとる命令。GPUはいくら並列演算器とはいえ、全部のコアが全く同じプログラムの行を実行しているわけではない。256並列なら0番目スレッドと255番目のスレッドではタイミングがズレているかもしれない。メモリにアクセスする際に順番がバラバラだと困ったことになるので、足並みをそろえてほしいときに使う。
注意としては1グループ=256スレッドの場合、256すべてのスレッドでGroupMemoryBarrierWithGroupSync();が実行されないと次の行に進まないということ。これは多分OpenCLやCUDAでも同じ考えだろう。



【総和アルゴリズムについて】
今回のやつを一応図解しておく。わかりやすくするために0~7の総和で考える。
reduction


最終的に共有メモリの0番目の要素に結果が集まる形である。



【とりあえずnumthreadsには64の倍数を指定しておくとなぜ良いのか】
前回の記事で
・numthreadsには1~1024までの数字しか指定できない
・とりあえずnumthreadsには64の倍数を指定しておくと良い
と書いているが、特に2番目、なぜそうしたほうが良いのかという理由はまだ説明できていない。その理解のためにはグループ、スレッドの抽象概念の理解とハードウェアのほうの理解両方が必要であり、いよいよ避けて通れなくなってきた。

これを理解するために早速あるプログラムの実験をしてみよう
C#(Host.cs)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class Host : MonoBehaviour
{
    public ComputeShader shader;
    ComputeBuffer A;
    int cnt;
    int time0;
    int time1;
    uint[] host_A;
    int[] k;
    int N;
    int[] timelist;
    int knum;
    int THREADNUM;
    void Start()
    {
        N = 11;
        THREADNUM = 256 * 1024;
        timelist = new int[N];
        host_A = new uint[THREADNUM];
        A = new ComputeBuffer(host_A.Length, sizeof(uint));
        k = new int[9];
        k[0] = shader.FindKernel("sharedmem_samp1");
        k[1] = shader.FindKernel("sharedmem_samp2");
        k[2] = shader.FindKernel("sharedmem_samp4");
        k[3] = shader.FindKernel("sharedmem_samp8");
        k[4] = shader.FindKernel("sharedmem_samp16");
        k[5] = shader.FindKernel("sharedmem_samp32");
        k[6] = shader.FindKernel("sharedmem_samp64");
        k[7] = shader.FindKernel("sharedmem_samp128");
        k[8] = shader.FindKernel("sharedmem_samp256");
        //引数をセット
        for (int i = 0; i < 9; i++)
        {
            shader.SetBuffer(k[i], "A", A);
        }
        cnt = 0;
        knum = 0;
    }
    

    void Update()
    {
        if (cnt < N) {
            gpuvoid();
        }
        
        if (cnt == N) {
            Array.Sort(timelist);//中央値を選択したい
            Debug.Log("グループスレッド数="+(1<<knum)+"\n計算時間        "+timelist[N/2]+"ms");
            
            knum++;
            if (knum == 9)
            {
                A.Release();
            }
            else
            {
                cnt = -1;
            }
        }

        cnt++;

    }

    void gpuvoid()
    {
        time0 = Gettime();
        // GPUで計算
        shader.Dispatch(k[knum], THREADNUM >> knum, 1, 1);
        // device to host
        A.GetData(host_A);
        time1 = Gettime() - time0;
        timelist[cnt] = time1;
    }

    //現在の時刻をms単位で取得
    int Gettime()
    {
        return DateTime.Now.Millisecond + DateTime.Now.Second * 1000
         + DateTime.Now.Minute * 60 * 1000 + DateTime.Now.Hour * 60 * 60 * 1000;
    }
}



ComputeShader
#pragma kernel sharedmem_samp1
#pragma kernel sharedmem_samp2
#pragma kernel sharedmem_samp4
#pragma kernel sharedmem_samp8
#pragma kernel sharedmem_samp16
#pragma kernel sharedmem_samp32
#pragma kernel sharedmem_samp64
#pragma kernel sharedmem_samp128
#pragma kernel sharedmem_samp256
RWStructuredBuffer<uint> A;
#define LPNM 2048


uint SinRandom(uint id) {
	uint x = id;
	float y;

	for (int i = 0; i < LPNM; i++) {
		y = sin(1.234f*(float)(x % 12345));
		y = 2.3456f / (y+1.0001f);
		x = (uint)(100000000.0f*y) + id;
	}
	return x;
}


[numthreads(256, 1, 1)]
void sharedmem_samp256(uint id : SV_DispatchThreadID) {
	A[id] = SinRandom(id);
}

[numthreads(128, 1, 1)]
void sharedmem_samp128(uint id : SV_DispatchThreadID) {
	A[id] = SinRandom(id);
}

[numthreads(64, 1, 1)]
void sharedmem_samp64(uint id : SV_DispatchThreadID) {
	A[id] = SinRandom(id);
}

[numthreads(32, 1, 1)]
void sharedmem_samp32(uint id : SV_DispatchThreadID) {
	A[id] = SinRandom(id);
}

[numthreads(16, 1, 1)]
void sharedmem_samp16(uint id : SV_DispatchThreadID) {
	A[id] = SinRandom(id);
}

[numthreads(8, 1, 1)]
void sharedmem_samp8(uint id : SV_DispatchThreadID) {
	A[id] = SinRandom(id);
}

[numthreads(4, 1, 1)]
void sharedmem_samp4(uint id : SV_DispatchThreadID) {
	A[id] = SinRandom(id);
}

[numthreads(2, 1, 1)]
void sharedmem_samp2(uint id : SV_DispatchThreadID) {
	A[id] = SinRandom(id);
}

[numthreads(1, 1, 1)]
void sharedmem_samp1(uint id : SV_DispatchThreadID) {
	A[id] = SinRandom(id);
}


https://github.com/toropippi/UintyGPGPU/tree/master/gpgpu_sample3a

これはGPU上で適当なランダムな値を生成してグローバルメモリに書き込むというサンプルだが、注目するべきはループの回数。2048ループと非常に多く、かつループ内には除算も入っており、これは完全に演算律速となる問題だ。これを256*1024並列でGPU上で実行する。その際全体の並列数は変えずに、グループスレッド数だけを変えて実行してみる。

AMD(RX 480)のグラボのとき
amdjikan


NVIDIA(MX150(Pascal))のグラボのとき
nvidia


見やすくグラフにしてみる
amd

nv


見やすくするために実行時間の逆数を棒グラフにしている。フレームレートを見ていると考えれば良い。横軸はグループスレッド数だ。

グループスレッド数を1→2→4と倍々に増やしていくと、それに応じて実行計算量が倍々に増えているのがわかる。そしてAMDでは64、NVIDIAでは32で頭打ちになる。
これらの結果からAMDのグラボではグループスレッド数を64、128、256にしたとき最大パフォーマンスが、NVIDIA(Pascal)のグラボではグループスレッド数を32、64、128、256にしたとき最大パフォーマンスが得られると経験的にわかった。
ComputeShaderは一応両方のベンダーのグラボで実行できるわけだから、多くの状況を想定するならグループスレッド数を最低でも64にしておくほうが安全ということになる。


ではなぜNVIDIAで32、AMDで64が頭打ちになるかというと、NVIDIAでいうところの「Warp」、AMDでいうところの「Wavefront」を理解する必要がある。自分は特に詳しいわけではないがざっくりいうとこんな感じ

Warp
・NVIDIAのGPUでは32スレッド分を1まとまりの単位として実行する。その単位がWarp
・Maxwell,Pascalアーキテクチャでは32スレッドを32CUDA Coreが1cicleで実行する
・Kepler,Volta,Turingアーキテクチャでは32スレッドを16CUDA Coreが2cicleで実行する
・16 or 32のスレッドが同じタイミングで同じ行のプログラムを実行する(多分)

Wavefront
・AMDのGPUでは64スレッド分を1まとまりの単位として実行する。その単位がWavefront
・64スレッドを16PEが4cicleで実行する(PE=CUDA coreみたいなもん)
・16スレッドが同じタイミングで同じ行のプログラムを実行する(多分)


だからグループスレッドに1を指定してしまうと、AMDのGPUなら1cicle目で16PEのうち15PEが空回りし2~4cicle目で16PE全部が空回りする。そしてNVIDIAのPascalだと32CUDA Core中31CUDA Coreが何もしないで空回りすることになってしまう。
こんな感じにとんでもない無駄を生む可能性があるため、この概念を少しでも理解し、グループスレッド数を指定できるようになりたい。
ここまでわかればとても奇数や素数を指定する気にはならないだろう。2のN乗でと考えると選択肢は64,128,256,512,1024に限られる。512,1024までいくと古いグラボやShaderModelによって実行できない場合があるので64,128,256の3つが現実的になるのかなと思う。
なお192とかは確かに64の倍数だけどなんか気持ち悪い・・・


【補足】
WindowsだとGPUのタスクが重すぎて2秒とか反応しないとOSが検知してドライバー強制終了させるのがデフォルトのようだ。
そんなことをされたらデバッグが捗らないのでここを参照してレジストリをいじっておく
https://support.borndigital.co.jp/hc/ja/articles/360000574634

UnityでGPGPUその2 並列計算と時間測定、コアレスアクセスなど

【今回やることはコレ】

naiseki

ベクトルの内積の計算をGPUで行う。要素数は65536*4=262144。ベクトルの各要素は1/1,1/2,1/3,1/4,1/5・・・であり、この場合内積の結果がpi26halfに収束することが分かっている。ベクトル長を長くするほど正確なπが求まる。(が、実際はfloat精度の桁数の問題ですぐ頭打ちになる)
Vec1とVec2は前回同様CPU側からデータを転送する。


各要素の掛け算のところをGPUで並列に行うわけだが、GPUの並列数を指定する個所は2つある。ComputeShader側の
[numthreads(x, y, z)]
とC#側の
shader.Dispatch(k, X, Y, Z);
だ。この場合x*X*y*Y*z*Z並列で処理が行われる。
じゃあ2つの使い分けはというと、これは説明しだすとかなり長くなるので次の記事で。最低限覚えておくべき知識は、「numthreadsには1~1024までの数字しか指定できない」「とりあえずnumthreadsには64の倍数を指定しておくと良い」というところ。

この知識をふまえ早速GPGPUでバリバリ高速化・・・したいところだがそこは抑えて、まずは「1並列」で処理し、その計算時間を計測する。

先ほどのHoge0を
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class Hoge0 : MonoBehaviour
{
    public ComputeShader shader;

    void Start()
    {
        int time0 = Gettime();
        int N = 262144;//=2^18
        float[] host_vec1 = new float[N];
        float[] host_vec2 = new float[N];
        float[] host_ans = new float[1];

        //初期値をセット
        for (int i=0;i< N; i++)
        {
            host_vec1[i] = 1.0f / (i + 1.0f);// 1/1 , 1/2 , 1/3 , 1/4・・・・
            host_vec2[i] = 1.0f / (i + 1.0f);// 1/1 , 1/2 , 1/3 , 1/4・・・・
        }

        ComputeBuffer vec1 = new ComputeBuffer(host_vec1.Length, sizeof(float));
        ComputeBuffer vec2 = new ComputeBuffer(host_vec2.Length, sizeof(float));
        ComputeBuffer ans = new ComputeBuffer(1, sizeof(float));

        int k = shader.FindKernel("CSMain");

        // host to device
        int time1 = Gettime() - time0;
        vec1.SetData(host_vec1);
        vec2.SetData(host_vec2);
        int time2 = Gettime() - time0;

        //引数をセット
        shader.SetBuffer(k, "vec1", vec1);
        shader.SetBuffer(k, "vec2", vec2);
        shader.SetBuffer(k, "ans", ans);

        // GPUで計算
        int time3 = Gettime() - time0;
        shader.Dispatch(k, 1, 1, 1);
        int time4 = Gettime() - time0;

        // device to host
        ans.GetData(host_ans);
        int time5 = Gettime() - time0;

        //計測時間表示
        Debug.Log("CPU→GPU転送前\t" + time1);
        Debug.Log("CPU→GPU転送後\t" + time2);
        Debug.Log("Dispatch前\t" + time3);
        Debug.Log("Dispatch後\t" + time4);
        Debug.Log("GPU→CPU転送後\t" + time5);

        //結果表示
        float calc_pi = Mathf.Sqrt(host_ans[0] * 6.0f);
        Debug.Log("π =" + calc_pi.ToString("f10"));

        //解放
        vec1.Release();
        vec2.Release();
        ans.Release();
    }

    // Update is called once per frame
    void Update()
    {

    }

    //現在の時刻をms単位で取得
    int Gettime()
    {
        return DateTime.Now.Millisecond + DateTime.Now.Second * 1000
         + DateTime.Now.Minute * 60 * 1000 + DateTime.Now.Hour * 60 * 60 * 1000;
    }
}



とし
HogeCS0を
#pragma kernel CSMain
//Read and Write
RWStructuredBuffer<float> vec1;
RWStructuredBuffer<float> vec2;
RWStructuredBuffer<float> ans;

[numthreads(1, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
	float s = 0.0;
	for (int i = 0; i < 262144; i++) {//=2^18
		s += vec1[i] * vec2[i];
	}
	ans[0] = s;
}

として実行。


kekka1

πが3.141くらいまで求まった。成功だ!

【計算速度の算出】

GPUの計算時間についてはどうか。一番最初の時刻を0msとしており、命令が実行される時点での時刻(ms)を右に表示してある。
上のはった画像を見るに、CPU→GPUのデータ転送は1ms。GPUの計算はなんと0msで行えているようだ。そしてGPU→CPUの転送に75msかかっている・・・ーーと思うがこれは間違い!

実際はGPUの計算に75msかかっている!Dispatch命令はGPUに向かってタスクを投げるだけであって、GPUの計算が終了するまで待つ命令ではない!!
【イメージ】
tasktime



だからどれだけGPUのタスクが重かろうと、
        // GPUで計算
        int time3 = Gettime() - time0;
        shader.Dispatch(k, 1, 1, 1);
        int time4 = Gettime() - time0;
このtime3とtime4の差はほとんどない。
個人的な感覚では0.3μsくらいだ。
そして
        // device to host
        ans.GetData(host_ans);
ここでGPUの計算が終わるのを待ち、そのあとGPU→CPUの転送が行われるというわけ。

ちなみにGPUの計算が終わるまで待つという命令(OpenCLで言うclFlush)はないのかというと、私が探した限りでは見つけられなかった。あればGPUの計算時間を直接計算できるのだが。今はGetDataで代用するしかないようだ。



計測時間についてさらに考察を深めてみよう。時間がない人は「ちゃんとした並列計算」まで飛ばしても構わない。

CPU→GPU、GPU→CPUのデータ転送はPCI Expressを経由している。
mem
2019年1月現在PCI Express Gen3.0がもっとも普及しておりGPUとの接続では基本PCI Express Gen3.0 x16となっているだろう。これは片方向16GB/sの帯域速度がでる。今回のプログラムではCPU→GPUに262144要素×4byte×2のデータ転送を行っているが、これはたったの2MBである。この転送にかかる時間は理論上0.122msとなり、実際に計測された1msでは遅いくらいだ(オーバーヘッドが含まるからおかしくはない)。


次にGPU処理部分の時間について考える。今回1並列で愚直に計算しており、せっかくRX480には2304個の演算器があるのに2303個は空回りしているという状態。ただ丁度よい機会なので1演算器あたりの性能をここで計算してみよう。

[numthreads(1, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
	float s = 0.0;
	for (int i = 0; i < 262144; i++) {//=2^18
		s += vec1[i] * vec2[i];
	}
	ans[0] = s;
}
さっきのCSMainだが、コードをみると計算の大部分はループ内の
		s += vec1[i] * vec2[i];
である。75msで262144ループなので1ループ当たり286nsかかっている。
大雑把にGPUのコアクロックを1Ghzとすると1clock=1nsなので、この1行に286clockもかかっていることになる!これは遅い!?
実はFP32演算器は1clockでa+b , a-b , a*b , a*b+cの計算を行うことができる。とするとこの行の計算自体は1clockで終わるはずで、vec1[i],vec2[i]のメモリアクセスがどうも原因であることに気づく。つまりメモリアクセスに約280clockかかっていて、その間FP32演算器が「待ちぼうけ」を喰らっているわけだ。なんという無駄か


【ちゃんとした並列計算】

ではまずComputeShaderの
[numthreads(1, 1, 1)]

[numthreads(32, 1, 1)]
に置き換える。
今まで話に出てきたメモリは「グローバルメモリ」といって、これに効率よくアクセスするには「コアレスアクセス」が必要だ。(グローバルメモリ=実体はGDDR5とかGDDR6とかHBM2)


[numthreads(32, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
	float s = 0.0;
	for (int i = 0; i < 8192; i++) {
		s += vec1[id.x + i*32] * vec2[id.x + i * 32];
	}
	
	ans[id.x] = s;
}
これがコアレスアクセスしている32並列のComputeShaderコードになる。1スレッドあたり8192回ループを行っているが、ベクトル長が262144要素で32並列なのでこうなる。
ここでvec1,vec2の添え字がid.x+i*32になっているのに注目。
iが0のとき
corelessacs
これがコアレスアクセス。一度に連続したデータをとってくることができる。

iが1のとき
corelessacs2
これもコアレスアクセス。
コアレスアクセスでは、一度のメモリアクセスに必要な時間(≒レイテンシ)は変わらないが、一度にとってこれるデータ量が増えるためTotalで見ると高速になる。この矢印がてんでバラバラな領域に向かっているとパフォーマンスが落ちる。(同じ領域内ならパフォーマンスは落ちない)


Hoge0(C#)も修正
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class Hoge0 : MonoBehaviour
{
    public ComputeShader shader;

    void Start()
    {
        int time0 = Gettime();
        int p = 32;//並列数
        int N = 65536* 4;
        float[] host_vec1 = new float[N];
        float[] host_vec2 = new float[N];
        float[] host_ans = new float[p];

        //初期値をセット
        for (int i=0;i< N; i++)
        {
            host_vec1[i] = 1.0f / (i + 1.0f);// 1/1 , 1/2 , 1/3 , 1/4・・・・
            host_vec2[i] = 1.0f / (i + 1.0f);// 1/1 , 1/2 , 1/3 , 1/4・・・・
        }

        ComputeBuffer vec1 = new ComputeBuffer(host_vec1.Length, sizeof(float));
        ComputeBuffer vec2 = new ComputeBuffer(host_vec2.Length, sizeof(float));
        ComputeBuffer ans  = new ComputeBuffer(host_ans.Length , sizeof(float));

        int k = shader.FindKernel("CSMain");

        // host to device
        int time1 = Gettime() - time0;
        vec1.SetData(host_vec1);
        vec2.SetData(host_vec2);
        int time2 = Gettime() - time0;

        //引数をセット
        shader.SetBuffer(k, "vec1", vec1);
        shader.SetBuffer(k, "vec2", vec2);
        shader.SetBuffer(k, "ans", ans);

        // GPUで計算
        int time3 = Gettime() - time0;
        shader.Dispatch(k, 1, 1, 1);
        int time4 = Gettime() - time0;

        // device to host
        ans.GetData(host_ans);
        int time5 = Gettime() - time0;

        //計測時間表示
        Debug.Log("CPU→GPU転送前\t" + time1);
        Debug.Log("CPU→GPU転送後\t" + time2);
        Debug.Log("Dispatch前  \t" + time3);
        Debug.Log("Dispatch後  \t" + time4);
        Debug.Log("GPU→CPU転送後\t" + time5);

        //結果表示
        float calc_pi = 0.0f;
        for (int i=0;i< p; i++)//最後の最後の結果はCPUで加算
        {
            calc_pi += host_ans[i];
        }
        calc_pi = Mathf.Sqrt(calc_pi * 6.0f);
        Debug.Log("π =" + calc_pi.ToString("f10"));

        //解放
        vec1.Release();
        vec2.Release();
        ans.Release();
    }

    // Update is called once per frame
    void Update()
    {

    }

    //現在の時刻をms単位で取得
    int Gettime()
    {
        return DateTime.Now.Millisecond + DateTime.Now.Second * 1000
            + DateTime.Now.Minute * 60 * 1000 + DateTime.Now.Hour * 60 * 60 * 1000;
    }
}

そして計算時間は
res2
13-6=7ms。結構縮んだ。もちろんこの7msの中にGPU→CPU転送時間も含まれているから実際はもう少し短い。
内積の最後の足し算だが、今まではGPU側のans[0]に結果を全部まとめていたが、今回はGPU側でans[0]~ans[31]に各スレッドの結果を代入し、CPU側でans[0]~ans[31]を合計している。
(GPU側で最後の32要素の合計をすることもできるがやや難易度が上がるので次の機会とする。)



次のSTEPとしてさらに並列数を増やしてみる。
C#側のDispatch関数で
        shader.Dispatch(k, 128, 1, 1);
こうして
128*32=4096並列で実行するとしよう。そうするとans[0]~ans[4095]まで必要になるがそこも修正して計算時間を計ってみると・・・
(コードは省略)
kekka4
7-6=1ms。は、速い!もはや正確に測定できてるのか疑うレベル。

今回の高速化ではコアレスアクセスのところはいじってないため、単に立ち上げるスレッド数が32→4096に増えたことによるものだろう。細かくは私もわからないが、多くのスレッドからメモリアクセス要求があったほうが、その帯域を使い切れるといったイメージだ。きっとさっきの32並列のやつだけではその帯域を全然使い切れてなかったということなのではなかろうか。


さて今度は計算時間をちゃんと測定するため、もっと計算の規模を大きくしなければいけなさそうだ。Nを65536*4から512倍の65536*2048にする。

Hoge0(C#)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class Hoge0 : MonoBehaviour
{
    public ComputeShader shader;

    void Start()
    {
        int time0 = Gettime();
        int bp = 32;//ComputeShaderで指定している並列数
        int gp = 128;//Dispatchで指定している並列数
        int N = 65536*2048;
        float[] host_vec1 = new float[N];
        float[] host_vec2 = new float[N];
        float[] host_ans = new float[bp*gp];

        //初期値をセット
        for (int i=0;i< N; i++)
        {
            host_vec1[i] = 1.0f / (i + 1.0f);// 1/1 , 1/2 , 1/3 , 1/4・・・・
            host_vec2[i] = 1.0f / (i + 1.0f);// 1/1 , 1/2 , 1/3 , 1/4・・・・
        }

        ComputeBuffer vec1 = new ComputeBuffer(host_vec1.Length, sizeof(float));
        ComputeBuffer vec2 = new ComputeBuffer(host_vec2.Length, sizeof(float));
        ComputeBuffer ans  = new ComputeBuffer(host_ans.Length , sizeof(float));

        int k = shader.FindKernel("CSMain");

        // host to device
        int time1 = Gettime() - time0;
        vec1.SetData(host_vec1);
        vec2.SetData(host_vec2);
        int time2 = Gettime() - time0;

        //引数をセット
        shader.SetBuffer(k, "vec1", vec1);
        shader.SetBuffer(k, "vec2", vec2);
        shader.SetBuffer(k, "ans", ans);

        // GPUで計算
        int time3 = Gettime() - time0;
        shader.Dispatch(k, gp, 1, 1);
        int time4 = Gettime() - time0;

        // device to host
        ans.GetData(host_ans);
        int time5 = Gettime() - time0;

        //計測時間表示
        Debug.Log("CPU→GPU転送前\t" + time1);
        Debug.Log("CPU→GPU転送後\t" + time2);
        Debug.Log("Dispatch前  \t" + time3);
        Debug.Log("Dispatch後  \t" + time4);
        Debug.Log("GPU→CPU転送後\t" + time5);

        //結果表示
        float calc_pi = 0.0f;
        for (int i=0;i< bp * gp; i++)//最後の最後の結果はCPUで加算
        {
            calc_pi += host_ans[i];
        }
        calc_pi = Mathf.Sqrt(calc_pi * 6.0f);
        Debug.Log("π =" + calc_pi.ToString("f10"));

        //解放
        vec1.Release();
        vec2.Release();
        ans.Release();
    }

    // Update is called once per frame
    void Update()
    {

    }

    //現在の時刻をms単位で取得
    int Gettime()
    {
        return DateTime.Now.Millisecond + DateTime.Now.Second * 1000
            + DateTime.Now.Minute * 60 * 1000 + DateTime.Now.Hour * 60 * 60 * 1000;
    }
}


HogeCS0(ComputeShader)

#pragma kernel CSMain
//Read and Write
RWStructuredBuffer<float> vec1;
RWStructuredBuffer<float> vec2;
RWStructuredBuffer<float> ans;

[numthreads(32, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
	float s = 0.0;
	for (int i = 0; i < 32768; i++) {//65536*2048/32/128=32768
		s += vec1[id.x + i * 4096] * vec2[id.x + i * 4096];
	}
	ans[id.x] = s;
}

これで実行すると
kekka5
2794-2503=261ms
512倍にしているので実際は261/512=0.51ms。最初の1並列プログラムと比較すると149倍高速になったことになる。

ソースはこちら
https://github.com/toropippi/Uinty_GPGPU_SharedMem_AtomicAdd/tree/master/gpgpu_sample2


πが全然収束してないがこれはfloat精度なので仕方ない。double型で計算し直したところ3.141592までは正確に計算できた。




最後に、コアレスアクセスでない方法を試す。
[numthreads(32, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
	float s = 0.0;
	for (int i = 0; i < 32768; i++) {//65536*2048/32/128=32768
		s += vec1[id.x* 32768 + i] * vec2[id.x* 32768 + i];
	}
	ans[id.x] = s;
}
今度は添え字がid.x*32768 + iになっている。

corelessacs3

iが0のとき、こうなる。iが1のときも同様、離れている個所へのメモリアクセスとなり、これはランダムアクセスといって効率が悪い。
この方式でやると・・・
kekka6
3713-2544=1169ms
やはり遅くなっている!

【まとめ】

ベクトルの内積をやるといって、結局メモリの話ばかりしてたような気がする。ただ実際、GPUの計算はグローバルメモリのアクセスがボトルネックになってくるので、パフォーマンスに直結するので大事なことだと思っている。
それにしても冗長な話が多すぎたかもしれない・・・文章力ないなぁ



※間違いご指摘あったらコメント下さい

UnityでGPGPUその1 C=A+B

UnityでGPGPU(Compute Shader)を扱ったブログや書籍が増えてきたものの依然GPUプログラミングの敷居は高い。GPUの計算結果をシェーダー(レンダリング)で使う場合、シェーダーの学習コストも高く、数値計算側とは別の壁になっている。
「数値計算」と「レンダリング」を切り分けて、数値計算部分のことについて簡単に書いた記事は少ないなと思っていた矢先、中国語で数値計算だけやっている記事があった。
https://blog.csdn.net/weixin_38884324/article/details/79284373
これこれ、こういうのが日本語でもきっと必要だ!


GPUプログラミングにおいてのHello Worldは、1+1の計算をすることだと思っている。
今回行うことはGPU上に4要素の配列変数A,B,Cを確保し、CPU側からAとBに1を代入、GPU上でC=A+Bを4並列で行い結果をCPU側にとってくるというもの。
eererytuyrrt






UnityでGPGPUを行うのに必要なのはたった3つ。
・C#コード
・ComputeShaderコード
・空のGameObject



では最初から
まずは適当に2Dでも3DでもどっちでもいいのでUnityプロジェクトを生成
no title


空のGameObjcetを作成する
1


Assetsフォルダ内にScriptsフォルダを作成して以下の2つを作成
・右クリック→Create→C# script→「Hoge0」
・右クリック→Create→Shader→Compute Shader→「HogeCS0」

以下のソースをコピー

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class Hoge0 : MonoBehaviour
{
    public ComputeShader shader;

    void Start()
    {
        float[] host_A = { 1f, 1f, 1f, 1f };
        float[] host_B = { 1f, 1f, 1f, 1f };
        float[] host_C = { 0f, 0f, 0f, 0f };

        ComputeBuffer A = new ComputeBuffer(host_A.Length, sizeof(float));
        ComputeBuffer B = new ComputeBuffer(host_B.Length, sizeof(float));
        ComputeBuffer C = new ComputeBuffer(host_C.Length, sizeof(float));

        int k = shader.FindKernel("CSMain");

        // host to device
        A.SetData(host_A);
        B.SetData(host_B);

        //引数をセット
        shader.SetBuffer(k, "A", A);
        shader.SetBuffer(k, "B", B);
        shader.SetBuffer(k, "C", C);

        // GPUで計算
        shader.Dispatch(k, 1, 1, 1);

        // device to host
        C.GetData(host_C);

        Debug.Log("GPU上で計算した結果");
        for (int i = 0; i < host_C.Length; i++)
        {
            Debug.Log(host_A[i] + ", " + host_B[i] + ", " + host_C[i]);
        }

        //解放
        A.Release();
        B.Release();
        C.Release();
    }


    // Update is called once per frame
    void Update()
    {

    }


}

ComputeShader
#pragma kernel CSMain
//Read and Write
RWStructuredBuffer<float> A;
RWStructuredBuffer<float> B;
RWStructuredBuffer<float> C;

[numthreads(4, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
	C[id.x] = A[id.x]+ B[id.x];
}

空のゲームオブジェクトにHoge0をアタッチ
2

ゲームオブジェクトをクリックしInspectorのとこのShaderにHogeCS0をセット
3


結果
4




解説

コード自体は中国語のサイトのやつをそれなりにパクっている。だが重要なことはすべてこの中につまっている。
ComputeBuffer A = new ComputeBuffer(host_A.Length, sizeof(float));
ここでGPU側のメモリを確保している。


int k = shader.FindKernel("CSMain");
ここでは、ComputeShader側のCSMainという関数とC#側のkを紐づけていると思えばよい。
今後このkを使ってC#側から命令発行したりする。

A.SetData(host_A);
host_AはCPU側で確保したメモリ。ここでCPU→GPUにデータ転送している。


shader.SetBuffer(k, "A", A);
ComputeShaderのCSMain関数で使うAは、さっきC#側で生成したAとまだ紐づいていないため、これで紐づける。1回紐づければ後はAの中身を変えようが解かれない。

shader.Dispatch(k, 1, 1, 1);
ここでやっとGPUに計算させることができる。kはCSMainと紐づいているのでCSMainを1回実行することになる。
今度はComputeShader側のコードみてみると
[numthreads(4, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
	C[id.x] = A[id.x]+ B[id.x];
}
numthreads(4, 1, 1)は4並列で計算するという意味。
id.xはスレッドidを意味している。
4並列なので0番目のスレッドではid.xが0、1番目のスレッドではid.xが1、2番目のスレッドではid.xが2、3番目のスレッドではid.xが3
このid.xの使い方が並列計算で最初慣れないところではあるが、これが1億並列になったとしてもこのid.xを使って並列計算するためこの概念は重要である。

C#コードに戻って
C.GetData(host_C);
ここでC→host_Cの転送が行われている。これによってはじめてCPU側で結果を確認することができる。ComputeBuffer「C」はGPU上に確保されているのでC#側からは見れないためだ。

A.Release();
GPU上に確保したメモリは最後解放しないとUnityに怒られる。ちなみに忘れてもプログラムを終了すれば自動的に解放される。





以上のことだけで、多くの数値計算コードをGPUに移植できるようになる。(ComputeShaderのコードの書き方はまた別であるが)




実行環境

OS:Windows 10
CPU:core i7 3820
メモリ:32GB
GPU:Radeon RX 480 (GDDR5 8G)
Unity:2018.3.9 Personal


ソースはこちらで公開https://github.com/toropippi/Uinty_GPGPU_SharedMem_AtomicAdd/tree/master/gpgpu_sample1
プロフィール

toropippi

記事検索
アクセスカウンター

    QRコード
    QRコード
    • ライブドアブログ