Closed11

UnityでTensorFlow Lite Android Neural Networks API Delegate対応する

Koki IbukuroKoki Ibukuro

Android Neural Networks API、(以下NNAPI)はAndroid組み込みの機械学習計算を高速化するAPI。

https://developer.android.com/ndk/guides/neuralnetworks

TensorFlow Liteでは、すでに簡単にNNAPIを有効にするオプションが付いていました。

https://github.com/tensorflow/tensorflow/blob/359c3cdfc5fabac82b3c70b3b6de2b0a8c16874f/tensorflow/lite/c/c_api_experimental.h#L156-L160

しかし、このオプションではデフォルト設定で有効になるだけで、キャッシュなどの設定にアクセス出来ません。

https://github.com/tensorflow/tensorflow/blob/359c3cdfc5fabac82b3c70b3b6de2b0a8c16874f/tensorflow/lite/delegates/nnapi/nnapi_delegate.h#L68-L79

今回はこちらの設定をC-APIとUnityから使えるように改造してみます。

Koki IbukuroKoki Ibukuro

手始めに、TensorFlow Liteのデフォルトのaarライブラリの今回必要となるNNAPI用のnnapi_delegate.hが含まれるのか確認していきましょう。

個人的には、TensorFlowの大規模なbazelファイルを読み解くよりおすすめの方法、バイナリを直接見る。をよく使います。

bazelの//tensorflow/lite/java:tensorflow-liteで生成されるtensorflow-lite.aarは複数プラットフォームのsoライブラリを含むzipファイルなので、まず拡張子を.zipに変えて、soファイルを見つけ、中身を見ていきます。

Koki IbukuroKoki Ibukuro

いた。

CからNNAPIを作ったり消したりするエントリポイント自体は、ストリップされずにsoファイルに残っているので、今回はC#からAPIアクセスできるようにしてあげれば良さそうです。

Koki IbukuroKoki Ibukuro

ざくっと決め打ちでとりあえずこんな感じか。char*のパスをc#で安全に設定するのが怪しい…。自身ない。

/* Copyright 2022 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/

// #if UNITY_ANDROID && !UNITY_EDITOR
#if UNITY_ANDROID // to test

using System;
using System.Runtime.InteropServices;
using TfLiteDelegate = System.IntPtr;

namespace TensorFlowLite
{
    public sealed class NNAPIDelegate : IDelegate
    {
        /// <summary>
        /// Preferred Power/perf trade-off. For more details please see
        /// ANeuralNetworksCompilation_setPreference documentation in :
        /// https://developer.android.com/ndk/reference/group/neural-networks.html
        /// </summary>
        [Flags]
        public enum ExecutionPreference : int
        {
            Undefined = -1,
            LowPower = 0,
            FastSingleAnswer = 1,
            SustainedSpeed = 2,
        };

        /// <summary>
        /// The Mirror of TfLiteNnapiDelegateOptions
        /// Use TfLiteNnapiDelegateOptionsDefault() for Default options.
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct Options
        {
            // Preferred Power/perf trade-off. Default to kUndefined.
            public ExecutionPreference executionPreference;

            // Selected NNAPI accelerator with nul-terminated name.
            // Default to nullptr, which implies the NNAPI default behavior: NNAPI
            // runtime is allowed to use all available accelerators. If the selected
            // accelerator cannot be found, NNAPI will not be used.
            // It is the caller's responsibility to ensure the string is valid for the
            // duration of the Options object lifetime.
            public IntPtr acceleratorName; // char*

            // The nul-terminated cache dir for NNAPI model.
            // Default to nullptr, which implies the NNAPI will not try caching the
            // compilation.
            public IntPtr cacheDir; // char*

            // The unique nul-terminated token string for NNAPI model.
            // Default to nullptr, which implies the NNAPI will not try caching the
            // compilation. It is the caller's responsibility to ensure there is no
            // clash of the tokens.
            // NOTE: when using compilation caching, it is not recommended to use the
            // same delegate instance for multiple models.
            public IntPtr modelToken; // char*

            // Whether to disallow NNAPI CPU usage. Default to 1 (true). Only effective on
            // Android 10 and above. The NNAPI CPU typically performs less well than
            // built-in TfLite kernels, but allowing CPU allows partial acceleration of
            // models. If this is set to true, NNAPI is only used if the whole model is
            // accelerated.
            public int disallowNnapiCpu;

            // Whether to allow fp32 computation to be run in fp16. Default to 0 (false).
            public int allowFp16;

            // Specifies the max number of partitions to delegate. A value <= 0 means
            // no limit. Default to 3.
            // If the delegation of the full set of supported nodes would generate a
            // number of partition greater than this parameter, only
            // <max_number_delegated_partitions> of them will be actually accelerated.
            // The selection is currently done sorting partitions in decreasing order
            // of number of nodes and selecting them until the limit is reached.
            public int maxNumberDelegatedPartitions;

            // The pointer to NNAPI support lib implementation. Default to nullptr.
            // If specified, NNAPI delegate will use the support lib instead of NNAPI in
            // Android OS.
            public IntPtr nnapi_support_library_handle; // void*
        }

        public TfLiteDelegate Delegate { get; private set; }

        public static Options DefaultOptions => TfLiteNnapiDelegateOptionsDefault();

        public NNAPIDelegate() : this(DefaultOptions)
        {
        }

        public NNAPIDelegate(Options options)
        {
            UnityEngine.Debug.Log("NNAPIDelegate Created");
            Delegate = TfLiteNnapiDelegateCreate(ref options);
        }

        public void Dispose()
        {
            if (Delegate != IntPtr.Zero)
            {
                TfLiteNnapiDelegateDelete(Delegate);
                Delegate = IntPtr.Zero;
            }
        }

        #region Externs
        internal const string TensorFlowLibrary = Interpreter.TensorFlowLibrary;

        // Returns a delegate that uses NNAPI for ops execution.
        // Must outlive the interpreter.
        [DllImport(TensorFlowLibrary)]
        private static extern unsafe TfLiteDelegate TfLiteNnapiDelegateCreate(ref Options options);

        // Returns TfLiteNnapiDelegateOptions populated with default values.
        [DllImport(TensorFlowLibrary)]
        private static extern unsafe Options TfLiteNnapiDelegateOptionsDefault();

        // Does any needed cleanup and deletes 'delegate'.
        [DllImport(TensorFlowLibrary)]
        private static extern unsafe void TfLiteNnapiDelegateDelete(TfLiteDelegate delegateHandle);

        #endregion // Externs
    }
}

#endif // UNITY_ANDROID && !UNITY_EDITOR
Koki IbukuroKoki Ibukuro

おや、とりあえずAndroid実機ビルドしようと思ったら、エラー。 年末だからと、OSをmacOS Ventura 13.1に上げた弊害が…。

Win32Exception: ApplicationName='/Applications/Unity/Hub/Editor/2020.3.41f1/PlaybackEngines/AndroidPlayer/SDK/tools/bin/sdkmanager', CommandLine='--list', CurrentDirectory='/Users/user/Desktop/BlankAndroidTest', Native error= mono-io-layer-error (5)
System.Diagnostics.Process.StartWithCreateProcess (System.Diagnostics.ProcessStartInfo startInfo) (at <39d8670c8f6e487fb992d3c14d754584>:0)
System.Diagnostics.Process.Start () (at <39d8670c8f6e487fb992d3c14d754584>:0)
(wrapper remoting-invoke-with-check) System.Diagnostics.Process.Start()
Koki IbukuroKoki Ibukuro

Venturaに上げたことでなんかパーミッションの変化したらしい。Unity同梱のAndroid SDKのバイナリたちに実行権限を与えてあげることでビルドできたっぽい。

sudo chmod -R 755 .

https://forum.unity.com/threads/cannot-compile-empty-project-with-il2cpp-for-android-on-2020-1-17f1.1020838/

USBキーボードをつないですぐも許可するためのダイアログがでていたらしく、普段クラムシェルモードで使っているため、数分、キーボードが認識しなくて格闘した。Ventunaの罠。

Koki IbukuroKoki Ibukuro

自分で作っといて、ちゃんと試してなかったけど、NNAPI、十分速いっすね。

OpenGLESのスレッドに悩まされなくていいなら、こっちのほうが都合がいいケースもありそう。

Koki IbukuroKoki Ibukuro

出来た気がする!

初回起動のときは、token_5504566449498313531.bin couldn't be opened for reading: No such file or directoryってログが出るが、

12-27 14:03:58.215 17049 17138 I Unity   : NNAPIDelegate Created
12-27 14:03:58.215 17049 17138 I Unity   : TensorFlowLite.NNAPIDelegate:.ctor(Options)
12-27 14:03:58.215 17049 17138 I Unity   : SsdSample:Start()
12-27 14:03:58.215 17049 17138 I Unity   :
12-27 14:03:58.230 17049 17138 I tflite  : Created TensorFlow Lite delegate for NNAPI.
12-27 14:03:58.261 17049 17138 W Unity   : TFLite Warning: File /storage/emulated/0/Android/data/com.asus4.tfliteunitysample/files/ssd-token_5504566449498313531.bin couldn't be opened for reading: No such file or directory
12-27 14:03:58.261 17049 17138 W tflite  : NNAPI SL driver did not implement SL_ANeuralNetworksDiagnostic_registerCallbacks!
12-27 14:03:58.359 17049 17138 I tflite  : Replacing 61 node(s) with delegate (TfLiteNnapiDelegate) node, yielding 4 partitions.
12-27 14:03:58.359 17049 17138 W tflite  : NNAPI SL driver did not implement SL_ANeuralNetworksDiagnostic_registerCallbacks!
12-27 14:03:58.365 17049 17138 W tflite  : NNAPI SL driver did not implement SL_ANeuralNetworksDiagnostic_registerCallbacks!

二回目起動のときは警告が出ないのでちゃんと読めているっぽい。

12-27 14:04:07.881 17049 17138 I Unity   : NNAPIDelegate Created
12-27 14:04:07.881 17049 17138 I Unity   : TensorFlowLite.NNAPIDelegate:.ctor(Options)
12-27 14:04:07.881 17049 17138 I Unity   : SsdSample:Start()
12-27 14:04:07.881 17049 17138 I Unity   :
12-27 14:04:07.897 17049 17138 I tflite  : Replacing 61 node(s) with delegate (TfLiteNnapiDelegate) node, yielding 4 partitions.
12-27 14:04:07.897 17049 17138 W tflite  : NNAPI SL driver did not implement SL_ANeuralNetworksDiagnostic_registerCallbacks!
12-27 14:04:07.903 17049 17138 W tflite  : NNAPI SL driver did not implement SL_ANeuralNetworksDiagnostic_registerCallbacks!
12-27 14:04:11.029 17049 17687 W Unity   : Camera2: Capture session failed Regular reason 1
12-27 14:04:11.029 17049 17687 W Unity   : Camera2: Capture session failed Regular reason 1

ちゃんとファイルを確認する。

$ adb shell
# で入って、実際のファイルを確認
$ ls /storage/emulated/0/Android/data/com.asus4.tfliteunitysample/files/
il2cpp ssd-token_5504566449498313531.bin

いた!

Koki IbukuroKoki Ibukuro

興味があったのでNNAPIのキャッシュって何が作られていているのを確認。

adb pull /storage/emulated/0/Android/data/com.asus4.tfliteunitysample/files/ssd-token_5504566449498313531.bin

adb pullでローカルに落としてきて、バイナリデータを拝見。

…、全然わからんですね。248bytesのファイル。
別のモデルを使うともう少し変わるのかな?

Koki IbukuroKoki Ibukuro

PR作って
https://github.com/asus4/tf-lite-unity-sample/pull/270

@stakemuraさん作のCI回し中。

ちなみに char*をc#から、IntPtrで渡すのは、

theIntPtr = Marshal.StringToHGlobalAuto(text);

で行けたぽい。ちょっとstringのライフサイクルが心配。だけど、IntPtrをピンすると実装がめんどくなるので、スマートな実装が思いつけばピンする感じかな… 。structで渡してコンストラクタを実行する間は問題ないと思うのだけど。C#職人に教えてほしい。

このスクラップは2022/12/29にクローズされました