TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

Unityで将棋アプリの開発 その3(Android対応)

前回WindowsでOnnxRuntimeで探索を動かせるようになったので、Androidでも動くようにした。

後で書くが、Unity Barracudaでもコルーチンを使うことでメインスレッドで非同期処理ができるようになったので、OnnxRuntimeは必須ではなくなった。

OnnxRuntimeをAndroidで動かすのに苦労したので、方法を残しておく。

Android版OnnxRuntimeインストール

NuGetForUnityでインストールするMicrosoft.ML.OnnxRuntime.Managedの他に、Android版OnnxRuntimeのバイナリが必要になる。

NuGetでMicrosoft.ML.OnnxRuntimeをインストールすると、Assets/Packeges/Microsoft.ML.OnnxRuntime.1.10.0の中にmicrosoft.ml.onnxruntime.1.10.0.nupkgがあるので、zipで解凍し、runtimes/android/native/にある「onnxruntime.aar」をAssets/Pluginsにコピーする。

Unityエディタで、「onnxruntime.aar」を選択し、インスペクタで、「Android」にチェックを入れる。

モデルのダウンロード

モデルファイル(.onnx)は、Assets/StreamingAssetsに格納する。

OnnxRuntimeは、モデルをファイルパスか、byte配列で受け取る必要があるが、Androidでは、StreamingAssetsにあるファイルは、jarで圧縮されるため、ファイルパスに指定できない。

Unityのドキュメントに記載のある通り、UnityWebRequestを使って読み込む必要がある。
ストリーミングアセット - Unity マニュアル

読み込み処理は、公式のサンプルにあるようにコルーチンを使用して読み込む。
Unity - Scripting API: Networking.UnityWebRequest.Get

    void Start()
    {
        StartCoroutine(InitSearcher());
    }


    IEnumerator InitSearcher()
    {
        using (UnityWebRequest webRequest = UnityWebRequest.Get(System.IO.Path.Combine(Application.streamingAssetsPath, modelFileName)))
        {
            // Request and wait for the desired page.
            yield return webRequest.SendWebRequest();
            byte[] model = webRequest.downloadHandler.data;
            _searcher = new UctSearcher(16, model);
        }
    }

ここで、StartCoroutineを使うとメインスレッドで非同期処理ができることに気付いたので、Unity Barracudaも非同期処理で使えることが分かったが、ひとまずOnnxRuntimeで続行する。

実機でテスト

Androidの実機(Pixcel5)でテストしてみた。
探索ノード64だと、1手3秒近くかかる。
探索ノード16に減らしても、2秒近くかかる。
探索のみのテストだと1秒以内になる想定なので、推論以外に遅くなる原因がありそうである。

Task.Runでマルチスレッドで実行していたのを疑い、上記で使い方の分かったコルーチンに変更してみた。
1プレイアウトごとに、yield returnを行いUIが止まらないようにした。

1秒くらいで実行できるようになった。
Unityは毎フレームごとゲームオブジェクトの処理がされるので、マルチスレッドを使うとスレッドのスイッチが頻発するので、Unityではマルチスレッドは使わない方がよいかもしれない。

全てメインスレッドで処理されるようになったので、Unity Barracudaで実行できるようになった。
Unity Barracudaでも体感速度はほぼ変わらなかった。

まとめ

OnnxRuntimeをAndoroidで動かすことができた。
試したのはCPUのみのため、NNAPI版も試してみたい。
Unity BarracudaではGPUにするとかえって遅くなったが、OnnxRuntimeのNNAPI版で速くなるか確認したい。
ただし、OnnxRuntimeが対応しているのはARM64のみのため、対応デバイスを増やすにはUnity Barracudaを使うのが無難そうだ。

実機テストしていて1手2秒だともっさり感を感じたが、ぴよ将棋でも確認したらLv8でも1手2秒以上かかっていた。
もっさり感を感じないのは、効果音と駒の移動のアニメーションがあるためだと気付いた。
駒のアニメーションがあるとその間思考できるので、待たされている時間を感じにくくなる効果がある。
自分のアプリでもぜひ駒のアニメーションは実装したい。

低レーティング帯での探索ノード数と強さの関係

以前に、R4000以上でのdlshogiの思考時間と強さの関係について調べた。

今回は、低レーティング帯で、dlshogiの探索ノード数と強さの関係について調査した。

調査方法

floodgateに、探索ノード数を変えて放流し、レーティングを調べた。
基本はバッチサイズ1、スレッド1で、探索ノード数が4096以上では、思考時間が3秒くらいになるようにバッチサイズを増やした。

比較のために、水匠5でもノード数を変えてレーティングを調査した。
水匠5も基本はスレッド1で、ノード数が12.8Mの場合だけ思考時間が3秒くらいになるようにスレッドを8にした。

結果

f:id:TadaoYamaoka:20220225210208p:plain

dlshogi
name nodes nodes比対数 rate rate差 nodes2倍あたり
dlshogi_pre17_p32768_b16th1 32768 3 3925 263 88
dlshogi_pre17_p4096_b2th1 4096 3 3662 292 97
dlshogi_pre17_p512_b1th1 512 3 3370 301 100
dlshogi_pre17_p64_b1th1 64 4 3069 513 128
dlshogi_pre17_p4_b1th1 4 2 2556 595 298
dlshogi_pre17_p1_b1th1 1 1961
水匠5
name nodes nodes比対数 rate rate差 nodes2倍あたり
suisho5_nodes12800k_th8 12800000 3 3818 308 103
suisho5_nodes1600k_th1 1600000 3 3510 342 114
suisho5_nodes200k_th1 200000 3 3168 466 155
suisho5_nodes25k_th1 25000 2.6 2702 576 218
suisho5_nodes4k_th1 4000 3.3 2126 855 257
suisho5_nodes400_th1 400 1271

考察

dlshogiも水匠5も、レーティングが低い方が、探索ノード数を倍にした際のレーティングの伸びが大きい。

dlshogiと水匠5では、水匠5の方がノード数を倍にした際の伸びが少し高い。
これは、前回の調査と同じ傾向である。

まとめ

低レーティング帯での探索ノード数と強さの関係について調査した。
調査の結果、レーティングが高くなるほど、レーティング差あたりの必要な探索数が増えることがわかった。
これは、レーティングの上昇に対して、必要な探索ノード数は指数的に増えていくことを示している。

dlshogiと水匠5では、水匠5の方がノード数を倍にした際の伸びが少し高いことがわかった。
しかし、レーティングが上がるほど、その差は少なくなる。

わずかな探索速度を伸ばす努力よりもモデルの精度を上げる方に注力すべきなのかもしれない。

Unityで将棋アプリの開発 その2(AIと対局)

先日から作り始めた将棋アプリをAIと対局できるようにした。
f:id:TadaoYamaoka:20220225005750p:plain

とりあえず指せるようにしただけで、形勢グラフなどはできていない。

Unity Barracuda使えなかった

事前に調査したUnity Barracudaだったが、メインスレッドでしか使えないという致命的な欠点があった。

CSharpBurst(CPU)だと、

UnityException: get_isDebugBuild can only be called from the main thread.

というエラーになり、Compute(GPU)だと、

UnityException: SupportsComputeShaders can only be called from the main thread.

というエラーになる。


UIを止めないために、探索はどうしても別スレッドで行う必要がある。
推論をメインスレッドで行うとスレッド間で通信して同期して処理が必要になるが、遅くなるのでそんなことはしたくない。

しかたないので、Unity BarracudaはあきらめてOnnxRuntimeを使うことにした。
OnnxRuntimeは、最近Androidにも対応したのでAndroidでも動くはずだが、実機ではまだ試していない。
また調査事項が増えてしまったorz

とりあえずWindowsで、UnityからOnnxRuntimeを使えるようにした。

こちらの情報を参考に、NuGetForUnityで、

をインストールした。

System.Memory 4.5.3がないというエラーがでるので、NuGetForUnityでインストールしようとしたが、検索結果に4.5.4しか表示されない。
そのため、直接NuGetのサイトからパッケージをダウンロードして、Pluginsフォルダに解凍したlib\netstandard2.0\にあるdllをコピーした。
NuGet Gallery | System.Memory 4.5.3
他にも、System.Runtime.CompilerServices.UnsafeとSystem.Buffersも必要なため、同様にインストールした。
NuGet Gallery | System.Runtime.CompilerServices.Unsafe 4.7.0
NuGet Gallery | System.Buffers 4.5.0

また、UnSafeのプロジェクト設定変更が必要である。
参考:【Unity】Unity 2018.1でunsafeなコードを使う - テラシュールブログ

.onnxは、StreamingAssetsに配置して、C#のコードからは、

Path.Combine(Application.streamingAssetsPath, modelFileName);

のようにしてパスを取得する。
参考:ストリーミングアセット - Unity マニュアル

これで、UnityからOnnxRuntimeで推論が行えるようになった。

Androidの場合は、OnnxRuntimeの別のバージョンが必要になるので、導入方法を調査しないといけない。

まとめ

とりあえず固定ノード数で探索ありのAIと対局できるようになった。
マルチスレッドで使えないことが判明したため、Unity Barracudaの使用はあきらめた。
代わりOnnxRuntimeで推論を行えるようにした。

完成までまだ先は長い・・・

Unityで将棋アプリの開発

先日試していたUnity Barracudaでのdlshogiのモデルを使用した探索が、Androidでもそこそこの強さになりそうなので、Android上でGUIで動かせるようにしたいと思っている。

接待dlshogiの機能を実装して、将棋の練習に使えるアプリにしたい。
また、1手に数秒使えば、R3000以上にできそうなので、人間には十分な強さになると思う。

ディープラーニング系の将棋AIは、探索ノード数を減らすと、弱くしつつ不自然な手を指さないという特徴がある。
従来型ではできなかったことなので、既にある将棋アプリとの差別化になればよいと考えている。


ということで、まずはUnityで将棋のGUIの実装はじめた。

Unityと1日中格闘しつつ、ようやくマウス(タップ)操作で合法手を指して、盤面を進められるようになった。
AIとの対局は未実装。

f:id:TadaoYamaoka:20220223193116p:plain


以下に、Unityで苦労した点をメモしておく(個人用メモ)。

解像度によって見た目が変わらないようにする

こちらの情報を参考に、カメラのSizeを動的に調整するようにした。
画面のサイズ(CameraのorthographicSize)をアスペクト比に合わせて自動で調整する【Unity】 - (:3[kanのメモ帳]

日本語テキストの表示

テキストの表示には、TextMeshProを使用したが、デフォルトで日本語のフォントが入っていない。
こちらの情報を参考に、Noto Sans JPを導入した。
【Unity】TextMeshProで日本語を表示する方法 | 怠惰な日々にさよならを

Font Asset Creatorの設定は、何度か調整して、以下の設定でうまくいった。
f:id:TadaoYamaoka:20220223193736p:plain

UIの解像度とゲームの解像度を合わせる

UIの座標は、デフォルトでピクセルで計算されるため、ゲーム画面上の座標を使用して任意の位置に文字を表示できない。
CanvasのRenderModeを「World Space」に変更することで、ゲーム画面の座標が使えるようになる。

クリック(タップ)の座標をゲームの座標に変換する

Input.mousePositionで取得できる座標は、端末のピクセル単位のため、ゲーム画面上の駒を選択するにはゲームの座標に変換が必要になる。
「Camera.main.ScreenToWorldPoint()」を使うとゲームの座標に変換できる。
参考:How to convert the mouse position to world space in Unity (2D + 3D) - Game Dev Beginner

Unity Barracudaでdlshogiのモデルを推論する その4(探索)

前回Androidでのモデルの推論速度を調べた。
今回は、MCTSで探索を行った際の探索速度を調べた。

前回、推論をバッチにしても同じデータ数を処理する時間がほとんど変わらないことが分かったため、MCTSの探索もバッチで並列化を行わないように実装した。
また、Andoroidではプレイアウト数は増やせないため、並列化するとVirtual Lossの影響が大きく精度が低くなるため、速度が変わらない場合、並列化はデメリットしかない。

測定コードの実装

C#MCTSの探索処理を実装して、初期局面で64プレイアウト行った際の時間を測定した。
モデルサイズ 5ブロック96フィルタのモデルを使用した。

    void Test4()
    {
        var sw = new System.Diagnostics.Stopwatch();

        var pos = new Position(Position.DefaultStartPositionSFEN);
        var searcher = new UctSearcher(64, modelAsset);

        sw.Start();
        Move move = searcher.Search(pos, pos.GetKey(), new Move[0]);
        sw.Stop();

        Debug.Log(move.ToUSI());
        text1.text = sw.ElapsedMilliseconds.ToString() + "\n" + move.ToUSI();

        searcher.Dispose();
    }

結果

Pixel5で、GPUおよびCPUで実行した場合の処理時間は以下の通りであった。

GPU/CPU 処理時間(ms)
GPUで実行(WorkerFactory.Type.Compute) 3918
CPUで実行(WorkerFactory.Type.CSharpBurst) 1373

前回調査したとおり、CPUの方が速くなっている。

64プレイアウトで、1手、1.4秒くらいで探索できることがわかった。

64プレイアウトの強さは、dlshogiの最新モデル(15ブロック224フィルタ)では、floodgateでR2971である。
5ブロック96フィルタでは、それよりも弱くなると思われる。

1手あたりの時間を増やせば強くできる。
ぴよ将棋のLV40だと1手10秒近く使っているので、同じくらい使えばスマホでも相当強くなりそうである。
dlshogiの最新モデルの512プレイアウトだと、floodgateでR3288である。

まとめ

AndoroidでMCTSで探索を行った場合の、処理時間を調べた。
バッチ並列化なしで、CPUで実行した場合、64プレイアウトで、初期局面で約1.4秒ということが分かった。

5ブロック96フィルタのモデルのため、dlshogiの教師データを使って十分に学習した場合にどれくらいの強さになるか別途確認したい。
また、モバイルに最適化されたMobileNetなどのモデルで、探索速度と精度がどうなるかも確認してみたい。

Unity Barracudaでdlshogiのモデルを推論する その3(Android)

Unity Barracudaによるdlshogiのモデルの推論をAndroidで試してみた。

Android対応

プロジェクト設定でプラットフォームをAndroidにするだけで、測定用コードは前回実装したをそのまま使用した。

測定端末には、Pixel5を使用した。

モデルサイズ

15ブロック224フィルタのモデルを試したところ、異常終了して実行できなかった。
モデルサイズを5ブロック96フィルタにしたところ、正常に実行できるようになった。

モデルサイズが大きいと、スマートフォンではメモリが不足するようである。

推論速度

前回と同じ局面数64000で試したところ、数分待っても応答が返ってこなかったので、局面数を6400に減らして測定した。
バッチサイズ128での推論に費やした時間は、86394 msという結果だった。
1バッチあたり、1727.8 msかかっている。

PCで、6400局面を推論した場合は、658 msであった。
Pixel5は、PCより131倍遅いことがわかった。

バッチサイズ

推論速度がバッチサイズに依存するか、バッチサイズを変えて測定してみた。

バッチサイズ 6400局面の推論時間(ms)
16 93540
32 89066
64 87064
128 86394

それほど変わっていないが、バッチサイズが大きいほど同じデータ量を処理する時間は短くなっている。
推論APIを呼び出すオーバーヘッドが関係していそうである。

CPUとの比較

CPUで推論した場合と比較した。
WorkerFactory.CreateWorkerに渡すWorker種別を、WorkerFactory.Type.ComputeからWorkerFactory.Type.CSharpBurstに変更した。

バッチサイズ128での6400局面の推論時間は、77559 msとなった。
GPUと比べて推論時間が89.7%になっている。

予想に反して、CPUの方が速いという結果になった。

調べたところ、GPUを使う場合は、WorkerFactory.Type.ComputePrecompiledを指定すると事前にモデルがコンパイルされるようだ。
WorkerFactory.Type.ComputePrecompiledに変えて再測定したところ、86224 msとなり、特に速くならなかった。

前回のPCの測定もWorkerFactory.Type.ComputePrecompiledで再測定したが、20315 msとなり、特に速くならなかった。

まとめ

Unity Barracudaによるdlshogiのモデルの推論を、Androidで試した。
PCと同じコードでビルド設定を変えるだけで、Androidで推論できることが確認できた。
ただし、dlshogiの15ブロック224フィルタのモデルでは異常終了し実行できなかった。
5ブロック96フィルタにすると実行できた。

推論速度を計測したところ、PC(RTX 3090)と比較して、推論速度は、131倍遅いということがわかった。
また、Pixel5ではGPUを使用するより、CPUで推論した方が少し速いという結果になった。

スマートフォンでは機種にもよると思うが、GPUによる高速化は期待できないため、CPUで実行することを前提に開発した方が良さそうである。

追記

モバイルで1手1秒使えるとした場合、どれくらいの強さか軽く確認してみた。
5ブロック96フィルタのモデルで、floodgateの2018年以降のR3800以上の棋譜+dlshgiと水匠の対局1200局の棋譜を学習させて、バッチサイズ1、固定プレイアウト64、1スレッド、df-pnなしでLesserkaiと10局対戦させてみたところ、10勝0敗であった。
64プレイアウトでも人間の初級者とプレイするには十分な強さで、スマートフォンでも遊べる将棋ソフトになりそうである。

Unity Barracudaでdlshogiのモデルを推論する その2(推論速度)

前回、Unity Barracudaでdlshogiのモデルを推論できることを確認した。

今回は、dlshogiで使用しているTensorRTと、推論速度を比較した。

モデルは、15ブロック224フィルタのdlshogiのモデルを使用した。
前回の記事で書いた通り、そのままではBarracudaでdlshogiのモデルが使用できないため、torch.flattenとviewに変更してonnxを作成し直している。

測定条件

floodgateの棋譜からサンプリングした64000局面をバッチサイズ128で推論した際の時間を比較した。

測定コード

Barracudaの計測は、Unity側でsfenで局面の一覧を読み込んで入力特徴量もC#側で作成できるようにした。
計測する範囲は、入力特徴量作成は含めず、推論部分のみにしている。

    void Test3()
    {
        Model runtimeModel = ModelLoader.Load(modelAsset, true);

        var worker = WorkerFactory.CreateWorker(WorkerFactory.Type.Compute, runtimeModel);

        int batchsize = 128;
        float[] inputData1 = new float[(int)CSharpShogi.Color.ColorNum * Features.MaxFeatures1Num * (int)Square.SquareNum * batchsize];
        float[] inputData2 = new float[Features.MaxFeatures2Num * (int)Square.SquareNum * batchsize];


        Position pos = new Position(Position.DefaultStartPositionSFEN);

        var sw = new System.Diagnostics.Stopwatch();

        TextAsset textAsset = Resources.Load<TextAsset>("floodgate.sfen");
        int i = 0;
        foreach (var sfen in textAsset.text.Split("\n"))
        {
            pos.Set(sfen);
            Features.MakeInputFeatures(pos, inputData1, inputData2, i);

            if (++i == batchsize)
            {
                sw.Start();

                Tensor inputTensor1 = new Tensor(batchsize, 9, 9, 62, inputData1);
                Tensor inputTensor2 = new Tensor(batchsize, 9, 9, 57, inputData2);

                worker.Execute(new Dictionary<string, Tensor> { { "input1", inputTensor1 }, { "input2", inputTensor2 } });

                Tensor outputTensorPolicy = worker.PeekOutput("output_policy");
                Tensor outputTensorValue = worker.PeekOutput("output_value");
                float[] outputPolicy = outputTensorPolicy.data.Download(new TensorShape(2187 * batchsize));
                float[] outputValue = outputTensorValue.data.Download(new TensorShape(batchsize));

                inputTensor1.Dispose();
                inputTensor2.Dispose();
                outputTensorPolicy.Dispose();
                outputTensorValue.Dispose();

                sw.Stop();

                Array.Clear(inputData1, 0, inputData1.Length);
                Array.Clear(inputData2, 0, inputData2.Length);

                i = 0;
            }
        }

        worker.Dispose();

        text1.text = sw.ElapsedMilliseconds.ToString();
    }

測定結果

時間(ms)
TensorRT 1687
Barracuda 20409

Barracudaの方が12倍遅いという結果になった。

TensorRTと比べると速度はでないようだ。

以前に測定したcuDNN(32bit)とTensorRT(16bit)の比較では、3.3倍程度だったので、cuDNNと比べてもかなり遅くなっている。

以前に測定したONNXRuntime(DirectML)とTensorRTの比較では、4.7倍程度だったので、ONNXRuntime(DirectML)と比べても遅い。

Barracudaは、入力TensorがNHWCになっているので、CPUやスマートフォン向けに最適化されているのかもしれない。

まとめ

Barracudaの推論速度をTensorRTと比較した。
dlsohgiの15ブロック224フィルタのモデルの推論では、Barracudaが12倍遅いという結果であった。
また、DirectMLを使用したOnnxRuntimeと比較しても遅いことがわかった。
WindowsAMDGPUにも対応したい場合は、OnnxRuntimeを使用した方がよい。
Barracudaを使う場面は、スマートフォン向けになりそうだ。