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

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

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

2019年01月

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

ベクトルの内積のGPU実装

naiseki

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

ということで各要素の掛け算のところを並列に行うことを考える。普通に考えると1コアで262144回の計算を2コアなら131072回、4コアなら65536回と並列度をあげればあげるほど1コアあたりの計算量が減って速く計算ができるはずだ。

まぁその通りなのだが、ある程度並列度を上げるとメモリ律速になって思うように速くならないことがある。というかそんなことだらけだ。
なので今回の記事ではそこのところも少し突っ込んでまとめていこうと思う。

GPUの並列数を指定する個所は2つある。ComputeShader側の
[numthreads(x, y, z)]
とC#側の
shader.Dispatch(k, X, Y, Z);
だ。
この場合x*X*y*Y*z*Z並列で処理が行われる。

・x,y,zのほうで1グループあたりのスレッド数を指定して並列化
・X,Y,Zのほうでグループ数を指定して並列化

ができる。

じゃあ2つの使い分けはというと、これは説明しだすとかなり長くなるので次の記事で。最低限覚えておくべき知識は、「numthreadsには1~1024までの数字しか指定できない」「とりあえずnumthreadsには64の倍数を指定しておくと良い」というところ。

この知識をふまえ早速GPGPUでバリバリ高速化・・・したいところだがそこは抑えて、まずは「1並列」で処理するコードを書いて答えを確認してみる。

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の計算時間計測の落とし穴

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では遅いくらいだ(オーバーヘッドが含まるからおかしくはない)。

clock単位の計算時間計測

次に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演算器が「待ちぼうけ」を喰らっているわけだ。なんという無駄か!

ちゃんとした並列計算

1グループあたりのスレッド数を増やして並列度を上げる

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

[numthreads(32, 1, 1)]
に置き換える。

コアレスアクセス

そして高速化において「連続したアドレスにアクセスすること」が鍵になる。上記の実験にて演算が1clockに対しメモリアクセスが280clockかかっていたことからも、メモリアクセスの高速化がいかに大事かわかる。

今まで話に出てきたメモリは「グローバルメモリ」といって、これに効率よくアクセスするには「コアレスアクセス」が必要だ。(グローバルメモリ=実体は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で見ると高速になる。この矢印がてんでバラバラな領域に向かっているとパフォーマンスが落ちる。同じ領域内なら矢印がバラバラでもパフォーマンスは落ちない ←ここ重要!

これができているかできていないかで、同じ並列数でも実行速度が全然違ってくる。記事の下のほうで実験してるが4-5倍ほど平気で変わってくる。

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

グループ数を増やして並列度を上げる

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

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

4096並列のときの計算時間再測定

さて今度は計算時間をちゃんと測定するため、もっと計算の規模を大きくしなければいけなさそうだ。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並列:75ms
32並列:7ms
4096並列:0.51ms

となり、最初の1並列プログラムと比較すると149倍高速になったことになる。

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

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

ランダムアクセスは遅い

最後に、コアレスアクセスでない方法を試す。先ほどの4096並列の512倍規模にしていたやつのコードを流用する。
[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
やはり遅くなっている!
261msから1169msなので4.48倍遅い。

まとめ

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

※本ブログで紹介しているソースコードはすべてCC0 パブリックドメインです。
ご自由にお使いください




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

UnityでGPGPUその1 C=A+B

UnityでGPGPU

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);
ここでGPU→CPUの転送が行われている。これによってはじめてCPU側で結果を確認することができる。ComputeBuffer「C」はGPU上に確保されているのでC#側からは見れないためだ。

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


以上のことだけで、多くの数値計算コードをGPUに移植できるようになる。

実行環境

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



※本ブログで紹介しているソースコードはすべてCC0 パブリックドメインです。
ご自由にお使いください
ソースはこちらで公開
https://github.com/toropippi/Uinty_GPGPU_SharedMem_AtomicAdd/tree/master/gpgpu_sample1

参考文献

https://blog.csdn.net/weixin_38884324/article/details/79284373
http://neareal.com/2601/



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


プロフィール

toropippi

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

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