動機
世の中Deep Neural Networkばやりだが、クラウドのAPIを叩くのはデータが持っていかれそうで怖いので、Androidのデバイス内でできないのかと思っていたら、AndroidにAndroid Neural Networks API(NNAPI)というものがあるというので、小手調べに、デバイスの情報を取り出してみた。
環境
- OS
- Android 12
- Hard
- Sense 4
- IDE
- Android Studio Dolphin | 2021.3.1 Patch 1
手順
- AndroidStudio で Native C++ を選んでプロジェクトを作る
2. gradle の設定
android {
...
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.22.1'
}
}
buildFeatures {
viewBinding true
}
ndkVersion "25.1.8937393"
}
Native C++ を選ぶと自動的に生成されるが、必要ならexternalNativeBuild
とndkVersion
のバージョンを、使用するもの(ダウンロードしたもの)に合わせておく。
表示で利用するので、viewBinding
を追記した。
3. Device数の取得
Native C++でプロジェクトを生成すると、native-lib.cppというものが作成されるので、これを利用することにする。その前に、CMakeでのコンパイルのための情報である CMakeLists.txt でNNAPIを使うための宣言を追加する。
target_link_libraries( # Specifies the target library.
...
# Link with libneuralnetworks.so for NN API
neuralnetworks
android
...)
次が本題のNNAPIである。まず、簡単なところで、Device数を取得してみる。NNAPIの関数はアドレス渡しで引数に指定した変数に返り値を入れる仕様になっている。C++の長ったらしい関数名は native-lib.cpp に例が作成されていると思うので、それを参考に設定する。
#include <android/NeuralNetworks.h>
extern "C" JNIEXPORT jint JNICALL
Java_com_yoo6309_firstndkapplication_MainActivity_getDeviceCount(
JNIEnv* env,
jobject /* this */) {
uint32_t numDevices;
ANeuralNetworks_getDeviceCount(&numDevices);
return (jint)numDevices;
}
Native C++ では JNI を使うことになっているので、NNAPIで取得したデバイス数であるnumDevices
を、javaのint型に対応するjintに変換してjava側に返すことになる。
これを使うjava側のプログラムは以下のようになる。関数をnative
で宣言するだけ。楽勝である。
int nDevice = getDeviceCount();
...
public native int getDeviceCount();
4. デバイス情報を取得する
デバイス情報としては、デバイス検出に挙げられた以下の関数がある。
ANeuralNetworksDevice_getFeatureLevel
ANeuralNetworksDevice_getName
ANeuralNetworksDevice_getType
ANeuralNetworksDevice_getVersion
1つずつ取得するのはだるいので、クラスを使いたいところだ。C++側からクラスのインスタンスを生成して値を返す方法については、夏休みの宿題は、JNIを参考にした。JNIの勉強が必要だ。また、NNAPIの実際の使い方は、TensorFlowのソースコードを参考にした。
まず、返り値用のクラスをjava側で宣言する。
public static class NNDevice {
long feature;
String name;
int type;
String version;
public NNDevice(long feature, String name, int type, String version) {
this.feature = feature;
this.name = name;
this.type = type;
this.version = version;
}
}
C++では、NNAPIの各関数で取得した値を、このクラスから生成したインスタンスの中に設定して返すことになる。FindClassのクラスの指定と、GetMethodIDでのmethod signatureの指定がわかりにくい。以下の例では、long, String, int, Stringの引数(入力)があり、出力がvoidという指定をしている。
extern "C" JNIEXPORT jobject JNICALL
Java_com_yoo6309_firstndkapplication_MainActivity_getDeviceParam(
JNIEnv* env,
jobject MainActivity /* this */,
jint i) {
ANeuralNetworksDevice* device;
int64_t featureLevel;
int32_t type;
const char* buffer;
int ans = ANeuralNetworks_getDevice((uint32_t)i, &device);
ans = ANeuralNetworksDevice_getFeatureLevel(device, &featureLevel);
ans = ANeuralNetworksDevice_getName(device, &buffer);
jstring name = env->NewStringUTF(buffer);
ans = ANeuralNetworksDevice_getType(device, &type);
ans = ANeuralNetworksDevice_getVersion(device, &buffer);
jstring version = env->NewStringUTF(buffer);
jclass deviceClass = env->FindClass("com/yoo6309/firstndkapplication/MainActivity$NNDevice");
jmethodID deviceConstructor;
deviceConstructor = env->GetMethodID(deviceClass, "<init>",
"(JLjava/lang/String;ILjava/lang/String;)V");
jobject ret = env->NewObject(deviceClass,
deviceConstructor,
featureLevel,
name,
type,
version);
return ret;
}
これを呼ぶjava側のプログラムは、以下のような感じ。
int i;
...
nnDevice = getDeviceParam(i);
...
public native NNDevice getDeviceParam(int i);
5. ソース全体
package com.yoo6309.firstndkapplication;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import com.yoo6309.firstndkapplication.databinding.ActivityMainBinding;
import java.util.Locale;
public class MainActivity extends AppCompatActivity {
// Used to load the 'firstndkapplication' library on application startup.
static {
System.loadLibrary("firstndkapplication");
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
NNDevice nnDevice;
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
int nDevice = getDeviceCount();
tv.setText(String.format(Locale.JAPAN, "Device Count: %d", nDevice));
StringBuilder msg = new StringBuilder();
for(int i = 0; i < nDevice; i++) {
nnDevice = getDeviceParam(i);
if (i > 0) msg.append("\n");
msg.append("feature:").append(nnDevice.feature).append("\nname:").append(nnDevice.name).append("\ntype:").append(nnDevice.type).append("\nversion:").append(nnDevice.version);
}
binding.deviceParamText.setText(msg.toString());
}
public static class NNDevice {
long feature;
String name;
int type;
String version;
public NNDevice(long feature, String name, int type, String version) {
this.feature = feature;
this.name = name;
this.type = type;
this.version = version;
}
}
/**
* A native method that is implemented by the 'firstndkapplication' native library,
* which is packaged with this application.
*/
public native int getDeviceCount();
public native NNDevice getDeviceParam(int i);
}
#include <jni.h>
#include <string>
#include <android/NeuralNetworks.h>
extern "C" JNIEXPORT jint JNICALL
Java_com_yoo6309_firstndkapplication_MainActivity_getDeviceCount(
JNIEnv* env,
jobject /* this */) {
uint32_t numDevices;
ANeuralNetworks_getDeviceCount(&numDevices);
return (jint)numDevices;
}
extern "C" JNIEXPORT jobject JNICALL
Java_com_yoo6309_firstndkapplication_MainActivity_getDeviceParam(
JNIEnv* env,
jobject MainActivity /* this */,
jint i) {
ANeuralNetworksDevice* device;
int64_t featureLevel;
int32_t type;
const char* buffer;
int ans = ANeuralNetworks_getDevice((uint32_t)i, &device);
ans = ANeuralNetworksDevice_getFeatureLevel(device, &featureLevel);
ans = ANeuralNetworksDevice_getName(device, &buffer);
jstring name = env->NewStringUTF(buffer);
ans = ANeuralNetworksDevice_getType(device, &type);
ans = ANeuralNetworksDevice_getVersion(device, &buffer);
jstring version = env->NewStringUTF(buffer);
jclass deviceClass = env->FindClass("com/yoo6309/firstndkapplication/MainActivity$NNDevice");
jmethodID deviceConstructor;
deviceConstructor = env->GetMethodID(deviceClass, "<init>",
"(JLjava/lang/String;ILjava/lang/String;)V");
jobject ret = env->NewObject(deviceClass,
deviceConstructor,
featureLevel,
name,
type,
version);
return ret;
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/sample_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="device count"
android:textAlignment="gravity"
app:layout_constraintBottom_toTopOf="@+id/device_param_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/device_param_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="device param"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/sample_text" />
</androidx.constraintlayout.widget.ConstraintLayout>
6. 実行
これをsense 4で実行したところ、以下のような出力を得た。Qualcommによれば、gpuはAdreno 618、htaは、"Qualcomm® Hexagon™ Tensor Accelerator"というものらしい。