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 並列計算と時間測定、コアレスアクセスなど