4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ひとりで完走_C# is GODAdvent Calendar 2024

Day 16

C#でGPGPU!? CUDAなんかいらなかった with ComputeSharp

Last updated at Posted at 2024-12-15

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で処理する部分は以下のようになります。

[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処理を呼び出すには以下を行います。

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とかで使われる計算も試してみたいですね。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?