C#でGPGPU！？　CUDAなんかいらなかった with ComputeSharp

# GPGPUについて
GPGPUとはGPUを使って大規模な計算を並列化しようという試みです。
これのおかげでAIが飛躍的に進歩したみたいです。
CUDAというNvidiaが出してるフレームワークが一番有名かもしれません。
ゲーム業界ではDirectXを使ったコンピュートシェーダーが有名ですね。
今回はGPGPUをC#で行っていきます。

# CUDAのめんどくささについて語りたい
GPGPUの主流であるCUDAなのですが、ハードウェアに最適化されててすごく高速みたいなのですが、Nvidia製のGPUを積んでるPCでしか使えないんですよね。
あとCUDAを使うにあたり、いろいろなドライバーをインストールしなくてはいけません。
この作業がとてもめんどくさいのです。
なので自分はこのめんどくささが軽減されるコンピュートシェーダーやDirectMLが好きです。
正直ある程度の速度が出せれば十分ですしね。

# ComputeShapを使ってGPGPUを簡略化しよう
今回C#でGPGPUを実装するにあたり、考えらるのはコンピュートシェーダーを使うことです。
しかしこのコンピュートシェーダー、シェーダーとあるようにhlslを記述し、シェーダーを書かなくてはならず、C#のみの実装はできません。

そこで登場したのが`ComputeSharp`になります。
これはC#でおなじみのソースジェネレーターをうまく生かし、C#コードからシェーダーをコンパイルしてGPGPUを実現しようというものです。
シェーダーをコンパイルする機能なので、コンピュートシェーダーではない、ピクセルシェーダーなんかも作れるのが、ほかの言語であるようなGPGPUライブラリとは一線を画しているところなのではないでしょうか。
あとやっぱりC#ってほかの言語よりも可読性高いので、ライブラリ特有の機能でなんじゃこれ？みたいなコードにならないのはいいですよね。

# GPU計算
今回GPU計算を行う例として、ランダムに生成した2048×2048の行列を二つ作り、その行列を掛け合わせるという、行列積をやってみたいと思います。

`ComputeShader`をNugetして作成ます。
GPUで処理する部分は以下のようになります。
```csharp
[ThreadGroupSize(DefaultThreadGroupSizes.XY)]//スレッドグループのサイズを指定します。行列なのでX*Yとしました。DefaultThredGroupSizesで何も考えなくてもいいのはありがたい
[GeneratedComputeShaderDescriptor]//ソースジェネレーターでシェーダーを作りますよのアトリビュート
// ソースジェネレーターを作るのでpartial。IComputeShaderを実装します。
public readonly partial struct MatrixMultiplicationKernel : IComputeShader
{
    // フィールド読み込みのみの場合はReadOnly,書き込むものについてはReadWriteとすること。
    // この他ReadOnlyTexture2D<T>とかあってシェーダーみたいでおもろいです。
    public readonly ReadOnlyBuffer<float> A;
    public readonly ReadOnlyBuffer<float> B;
    public readonly ReadWriteBuffer<float> C;
    public readonly int N;

    // コンストラクタ。本体C#のメソッド側から呼ばれます。
    public MatrixMultiplicationKernel(ReadOnlyBuffer<float> a, ReadOnlyBuffer<float> b,  ReadWriteBuffer<float> c, int n)
    {
        // 本体C#側から受け取った情報をフィールドに入れる
        A = a;
        B = b;
        C = c;
        N = n;
    }
    // この処理が実行されます。
    public void Execute()
    {
        // ThreadIdsからスレッドIDが取得できるので、並列で処理するときはこれを使う
        int row = ThreadIds.X;
        int col = ThreadIds.Y;

        float sum = 0;
        for (int k = 0; k < N; k++)
        {
            // 担当した部分だけの行列の値を取得している。
            sum += A[row * N + k] * B[k * N + col];
        }
        // 担当行列の部分に入れ込む
        C[row * N + col] = sum;
    }        
}
```
`void Execute()`が`IComputeShader`の実装に必要な関数となっていてこの中の処理を行います。
もちろん普通のシェーダーのように関数を外出ししても`Ececute()`の中で呼ばれてれば問題ないですね。
こういうシェーダーをC#っぽくかけるのはなかなか革命なんじゃないかと思います。
今回はfloat[N,N]のマトリックスではなく、float[N*N]で表現しています。

# 呼び出し
上記で作成したGPGPU処理を呼び出すには以下を行います。
```csharp
using ComputeSharp;
using ComputeSharpTest;
using System.Diagnostics;

// 行列の縦横の長さこれは2048×2048という行列という意味
int N = 2048;

// ランダムで作成された2048×2048の行列matrixAを作成
float[] matrixA = SetMatrix(N);
//Console.WriteLine("matrixA :");//どんな行列か見るように作ったが、サイズがでかいと馬鹿みたいになるのでコメントアウト
//for(int i = 0; i < N; i++)
//{
//    for(int j = 0; j < N; j++)
//    {
//        Console.Write($"{matrixA[i * N + j]:F2}");
//    }
//    Console.WriteLine();
//}

// ランダムで作成された2048×2048の行列matrixBを作成
float[] matrixB = SetMatrix(N);
//Console.WriteLine("matrixB :");
//for (int i = 0; i < N; i++)
//{
//    for (int j = 0; j < N; j++)
//    {
//        Console.Write($"{matrixB[i * N + j]:F2},");
//    }
//    Console.WriteLine();
//}

// 空の2048×2048の行列matrixBを作成(結果取得用)
float[] matrixC = new float[N * N];

// 時間を測ります
Stopwatch sw = new Stopwatch();
Console.WriteLine($"計測開始");
sw.Start();

// C#のfloat[]をGPUで使えるようにbufferに変換します。GPU用にメモリを移し替えてる的な
// メモリアロケーターでここでしか使わないので`using`を使うこと使わないとメモリリーク
using (var bufferA = GraphicsDevice.GetDefault().AllocateReadOnlyBuffer(matrixA))
using(var bufferB = GraphicsDevice.GetDefault().AllocateReadOnlyBuffer(matrixB))
using(var bufferC = GraphicsDevice.GetDefault().AllocateReadWriteBuffer<float>(matrixC.Length))
{
    // これで上記で作ったコンピュートシェーダーを呼び出す(x,y,shader)x*y回MatrixMultiplicationKernelを実行
    // コンストラクタには上記シェーダーに書いた引数を入れ込む
    GraphicsDevice.GetDefault().For(N, N, new MatrixMultiplicationKernel(bufferA, bufferB, bufferC, N));

    // 参照渡しのため、与えたbufferCに値が入れられる。それをmatrixCにコピーする
    bufferC.CopyTo(matrixC);
}
sw.Stop();

// コピーされたmatrixCを確認。馬鹿みたいに数が多いのでコメントアウト
//Console.WriteLine("Result Matrix C:");
//for (int i = 0; i < N; i++)
//{
//    for (int j = 0; j < N; j++)
//    {
//        Console.Write($"{matrixC[i * N + j]} ");
//    }
//    Console.WriteLine();
//}

// 処理時間を表示
Console.WriteLine($"経過時間： {sw.Elapsed}");
Console.WriteLine($"経過時間(ms)： {sw.ElapsedMilliseconds}");

// ランダムな数値の[size*size]の行列を作る(matrixA,matrixB)を作るのに使っている
float[] SetMatrix(int size)
{
    var matrix = new float[size * size];
    Random rando = new Random();
    for(int i = 0;i < size;i++)
    {
        for(int j = 0;j < size;j++)
        {
            matrix[i * N + j] = (float) (rando.NextDouble() * 10);
        }
    }
    return matrix;
}
```
ランダムな行列作成もComputeSharpでやろうかと思いましたが、ChatGPTによるとCPUでやった方が早いとのことなのでCPU側で作成してます。

だいたいこれで157msとかでした。
3×3や100×100のときも140ms前後だったので、計算処理よりもCPUからGPUにメモリを渡すところがオーバーヘッドみたいですね。
いかにもなGPU処理っぽい結果がでました。

# まとめ
`ComputeSharp`を使えばCUDAを使わずともGPGPUができるようになります。
めちゃくちゃお手軽で、C#にもhlslにも寄り添った書き味で面白いです。
今後はフーリエ変換だったり、AIとかで使われる計算も試してみたいですね。