• Tutorials >
  • Making Native Android Application that uses PyTorch prebuilt libraries
Shortcuts

Making Native Android Application that uses PyTorch prebuilt libraries

Author: Ivan Kobzarev

In this recipe, you will learn:

  • How to make an Android Application that uses LibTorch API from native code (C++).

  • How to use within this application TorchScript models with custom operators.

The full setup of this app you can find in PyTorch Android Demo Application Repository.

Setup

You will need a Python 3 environment with the following packages (and their dependencies) installed:

  • PyTorch 1.6

For Android development, you will need to install:

  • Android NDK

wget https://dl.google.com/android/repository/android-ndk-r19c-linux-x86_64.zip
unzip android-ndk-r19c-linux-x86_64.zip
export ANDROID_NDK=$(pwd)/android-ndk-r19c
  • Android SDK

wget https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
unzip sdk-tools-linux-3859397.zip -d android_sdk
export ANDROID_HOME=$(pwd)/android_sdk
  • Gradle 4.10.3

Gradle is the most widely used build system for android applications, and we will need it to build our application. Download it and add to the path to use gradle in the command line.

wget https://services.gradle.org/distributions/gradle-4.10.3-bin.zip
unzip gradle-4.10.3-bin.zip
export GRADLE_HOME=$(pwd)/gradle-4.10.3
export PATH="${GRADLE_HOME}/bin/:${PATH}"
  • JDK

Gradle requires JDK, you need to install it and set environment variable JAVA_HOME to point to it. For example you can install OpenJDK, following instructions.

  • OpenCV SDK for Android

Our custom operator will be implemented using the OpenCV library. To use it for Android, we need to download OpenCV SDK for Android with prebuilt libraries. Download from OpenCV releases page. Unzip it and set the environment variable OPENCV_ANDROID_SDK to it.

Preparing TorchScript Model With Custom C++ Operator

TorchScript allows using custom C++ operators, to read about it with details you can read the dedicated tutorial.

As a result, you can script the model that uses custom op, that uses OpenCV cv::warpPerspective function.

import torch
import torch.utils.cpp_extension

print(torch.version.__version__)
op_source = """
#include <opencv2/opencv.hpp>
#include <torch/script.h>

torch::Tensor warp_perspective(torch::Tensor image, torch::Tensor warp) {
  cv::Mat image_mat(/*rows=*/image.size(0),
                    /*cols=*/image.size(1),
                    /*type=*/CV_32FC1,
                    /*data=*/image.data_ptr<float>());
  cv::Mat warp_mat(/*rows=*/warp.size(0),
                   /*cols=*/warp.size(1),
                   /*type=*/CV_32FC1,
                   /*data=*/warp.data_ptr<float>());

  cv::Mat output_mat;
  cv::warpPerspective(image_mat, output_mat, warp_mat, /*dsize=*/{64, 64});

  torch::Tensor output =
    torch::from_blob(output_mat.ptr<float>(), /*sizes=*/{64, 64});
  return output.clone();
}

static auto registry =
  torch::RegisterOperators("my_ops::warp_perspective", &warp_perspective);
"""

torch.utils.cpp_extension.load_inline(
    name="warp_perspective",
    cpp_sources=op_source,
    extra_ldflags=["-lopencv_core", "-lopencv_imgproc"],
    is_python_module=False,
    verbose=True,
)

print(torch.ops.my_ops.warp_perspective)


@torch.jit.script
def compute(x, y):
    if bool(x[0][0] == 42):
        z = 5
    else:
        z = 10
    x = torch.ops.my_ops.warp_perspective(x, torch.eye(3))
    return x.matmul(y) + z


compute.save("compute.pt")

This snippet generates compute.pt file which is TorchScript model that uses custom op my_ops.warp_perspective.

You need to have installed OpenCV for development to run it. For Linux systems that can be done using the next commands: CentOS:

yum install opencv-devel

Ubuntu:

apt-get install libopencv-dev

Making Android Application

After we succeeded in having compute.pt, we want to use this TorchScript model within Android application. Using general TorchScript models (without custom operators) on Android, using Java API, you can find here. We can not use this approach for our case, as our model uses a custom operator(my_ops.warp_perspective), default TorchScript execution will fail to find it.

Registration of ops is not exposed to PyTorch Java API, thus we need to build Android Application with native part (C++) and using LibTorch C++ API to implement and register the same custom operator for Android. As our operator uses the OpenCV library - we will use prebuilt OpenCV Android libraries and use the same functions from OpenCV.

Let’s start creating Android application in NativeApp folder.

mkdir NativeApp
cd NativeApp

Android Application Build Setup

Android Application build consists of the main gradle part and native build CMake part. All the listings here are the full file listing, that if to recreate the whole structure, you will be able to build and install the result Android Application without any code additions.

Gradle Build Setup

We will need to add gradle setup files: build.gradle, gradle.properties, settings.gradle. More about Android Gradle build configurations you can find here.

NativeApp/settings.gradle

include ':app'

NativeApp/gradle.properties

android.useAndroidX=true
android.enableJetifier=true

NativeApp/build.gradle

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

In NativeApp/build.gradle we specify Android gradle plugin version 3.5.0. This version is not recent. Still, we use it as PyTorch android gradle builds use this version.

NativeApp/settings.gradle shows that out project contains only one module - app, which will be our Android Application.

mkdir app
cd app

NativeApp/app/build.gradle

apply plugin: 'com.android.application'

repositories {
  jcenter()
  maven {
    url "https://oss.sonatype.org/content/repositories/snapshots"
  }
}

android {
  configurations {
    extractForNativeBuild
  }
  compileSdkVersion 28
  buildToolsVersion "29.0.2"
  defaultConfig {
    applicationId "org.pytorch.nativeapp"
    minSdkVersion 21
    targetSdkVersion 28
    versionCode 1
    versionName "1.0"
    externalNativeBuild {
      cmake {
        arguments "-DANDROID_STL=c++_shared"
      }
    }
  }
  buildTypes {
    release {
      minifyEnabled false
    }
  }
  externalNativeBuild {
    cmake {
      path "CMakeLists.txt"
    }
  }
  sourceSets {
    main {
      jniLibs.srcDirs = ['src/main/jniLibs']
    }
  }
}

dependencies {
  implementation 'com.android.support:appcompat-v7:28.0.0'

  implementation 'org.pytorch:pytorch_android:1.6.0-SNAPSHOT'
  extractForNativeBuild 'org.pytorch:pytorch_android:1.6.0-SNAPSHOT'
}

task extractAARForNativeBuild {
  doLast {
    configurations.extractForNativeBuild.files.each {
      def file = it.absoluteFile
      copy {
        from zipTree(file)
        into "$buildDir/$file.name"
        include "headers/**"
        include "jni/**"
      }
    }
  }
}

tasks.whenTaskAdded { task ->
  if (task.name.contains('externalNativeBuild')) {
    task.dependsOn(extractAARForNativeBuild)
  }
}

This gradle build script registers dependencies on pytorch_android snapshots, that are published on nightly channels.

As they are published to nexus sonatype repository - we need to register that repository: https://oss.sonatype.org/content/repositories/snapshots.

In our application we need to use LibTorch C++ API in our application native build part. For this, we need access to prebuilt binaries and headers. They are prepacked in PyTorch Android builds, which is published in Maven repositories.

To use PyTorch Android prebuilt libraries from gradle dependencies (which is aar files) - we should add registration for configuration extractForNativeBuild, add this configuration in dependencies and put its definition in the end.

extractForNativeBuild task will call extractAARForNativeBuild task that unpacks pytorch_android aar to gradle build directory.

Pytorch_android aar contains LibTorch headers in headers folder and prebuilt libraries for different Android abis in jni folder: $ANDROID_ABI/libpytorch_jni.so, $ANDROID_ABI/libfbjni.so. We will use them for our native build.

The native build is registered in this build.gradle with lines:

android {
  ...
  externalNativeBuild {
    cmake {
      path "CMakeLists.txt"
    }
}
...
defaultConfig {
  externalNativeBuild {
    cmake {
      arguments "-DANDROID_STL=c++_shared"
    }
  }
}

We will use CMake configuration for a native build. Here we also specify that we will dynamically link with STL, as we have several libraries. More about this, you can find here.

Native Build CMake Setup

The native build will be configured in NativeApp/app/CMakeLists.txt:

cmake_minimum_required(VERSION 3.4.1)
set(TARGET pytorch_nativeapp)
project(${TARGET} CXX)
set(CMAKE_CXX_STANDARD 14)

set(build_DIR ${CMAKE_SOURCE_DIR}/build)

set(pytorch_testapp_cpp_DIR ${CMAKE_CURRENT_LIST_DIR}/src/main/cpp)
file(GLOB pytorch_testapp_SOURCES
  ${pytorch_testapp_cpp_DIR}/pytorch_nativeapp.cpp
)

add_library(${TARGET} SHARED
    ${pytorch_testapp_SOURCES}
)

file(GLOB PYTORCH_INCLUDE_DIRS "${build_DIR}/pytorch_android*.aar/headers")
file(GLOB PYTORCH_LINK_DIRS "${build_DIR}/pytorch_android*.aar/jni/${ANDROID_ABI}")

target_compile_options(${TARGET} PRIVATE
  -fexceptions
)

set(BUILD_SUBDIR ${ANDROID_ABI})

find_library(PYTORCH_LIBRARY pytorch_jni
  PATHS ${PYTORCH_LINK_DIRS}
  NO_CMAKE_FIND_ROOT_PATH)
find_library(FBJNI_LIBRARY fbjni
  PATHS ${PYTORCH_LINK_DIRS}
  NO_CMAKE_FIND_ROOT_PATH)

# OpenCV
if(NOT DEFINED ENV{OPENCV_ANDROID_SDK})
  message(FATAL_ERROR "Environment var OPENCV_ANDROID_SDK is not set")
endif()

set(OPENCV_INCLUDE_DIR "$ENV{OPENCV_ANDROID_SDK}/sdk/native/jni/include")

target_include_directories(${TARGET} PRIVATE
 "${OPENCV_INCLUDE_DIR}"
  ${PYTORCH_INCLUDE_DIRS})

set(OPENCV_LIB_DIR "$ENV{OPENCV_ANDROID_SDK}/sdk/native/libs/${ANDROID_ABI}")

find_library(OPENCV_LIBRARY opencv_java4
  PATHS ${OPENCV_LIB_DIR}
  NO_CMAKE_FIND_ROOT_PATH)

target_link_libraries(${TARGET}
  ${PYTORCH_LIBRARY}
  ${FBJNI_LIBRARY}
  ${OPENCV_LIBRARY}
  log)

Here we register only one source file pytorch_nativeapp.cpp.

On the previous step in NativeApp/app/build.gradle, the task extractAARForNativeBuild extracts headers and native libraries to build directory. We set PYTORCH_INCLUDE_DIRS and PYTORCH_LINK_DIRS to them.

After that, we find libraries libpytorch_jni.so and libfbjni.so and add them to the linking of our target.

As we plan to use OpenCV functions to implement our custom operator my_ops::warp_perspective - we need to link to libopencv_java4.so. It is packaged in OpenCV SDK for Android, that was downloaded on the Setup step. In this configuration, we find it by environment variable OPENCV_ANDROID_SDK.

We also link with log library to be able to log our results to Android LogCat.

As we link to OpenCV Android SDK’s libopencv_java4.so, we should copy it to NativeApp/app/src/main/jniLibs/${ANDROID_ABI}

cp -R $OPENCV_ANDROID_SDK/sdk/native/libs/* NativeApp/app/src/main/jniLibs/

Adding the model file to the application

To package the TorschScript model compute.pt within our application we should copy it to assets folder:

mkdir -p NativeApp/app/src/main/assets
cp compute.pt NativeApp/app/src/main/assets

Android Application Manifest

Every Android application has a manifest. Here we specify the application name, package, main activity.

NativeApp/app/src/main/AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.pytorch.nativeapp">

    <application
        android:allowBackup="true"
        android:label="PyTorchNativeApp"
        android:supportsRtl="true"
        android:theme="@style/Theme.AppCompat.Light.DarkActionBar">

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Sources

Java Code

Now we are ready to implement our MainActivity in

NativeApp/app/src/main/java/org/pytorch/nativeapp/MainActivity.java:

package org.pytorch.nativeapp;

import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class MainActivity extends AppCompatActivity {

  private static final String TAG = "PyTorchNativeApp";

  public static String assetFilePath(Context context, String assetName) {
    File file = new File(context.getFilesDir(), assetName);
    if (file.exists() && file.length() > 0) {
      return file.getAbsolutePath();
    }

    try (InputStream is = context.getAssets().open(assetName)) {
      try (OutputStream os = new FileOutputStream(file)) {
        byte[] buffer = new byte[4 * 1024];
        int read;
        while ((read = is.read(buffer)) != -1) {
          os.write(buffer, 0, read);
        }
        os.flush();
      }
      return file.getAbsolutePath();
    } catch (IOException e) {
      Log.e(TAG, "Error process asset " + assetName + " to file path");
    }
    return null;
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    final String modelFileAbsoluteFilePath =
        new File(assetFilePath(this, "compute.pt")).getAbsolutePath();
    NativeClient.loadAndForwardModel(modelFileAbsoluteFilePath);
  }
}

In the previous step, when we copied our compute.pt to NativeApp/app/src/main/assets that file became an Android application asset, which will be packed in application. Android system provides only stream access to it. To use this module from LibTorch, we need to materialize it as a file on the disk. assetFilePath function copies data from the asset input stream, writes it on the disk, and returns absolute file path for it.

OnCreate method of Activity is called just after Activity creation. In this method, we call assertFilePath and call NativeClient class that will dispatch it to native code through JNI call.

NativeClient is a helper class with an internal private class NativePeer, which is responsible for working with the native part of our application. It has a static block that will load libpytorch_nativeapp.so, that is build with CMakeLists.txt that we added on the previous step. The static block will be executed with the first reference of NativePeer class. It happens in NativeClient#loadAndForwardModel.

NativeApp/app/src/main/java/org/pytorch/nativeapp/NativeClient.java:

package org.pytorch.nativeapp;

public final class NativeClient {

  public static void loadAndForwardModel(final String modelPath) {
    NativePeer.loadAndForwardModel(modelPath);
  }

  private static class NativePeer {
    static {
      System.loadLibrary("pytorch_nativeapp");
    }

    private static native void loadAndForwardModel(final String modelPath);
  }
}

NativePeer#loadAndForwardModel is declared as native, it does not have definition in Java. Call to this method will be re-dispatched through JNI to C++ method in our libpytorch_nativeapp.so, in NativeApp/app/src/main/cpp/pytorch_nativeapp.cpp.

Native code

Now we are ready to write a native part of our application.

NativeApp/app/src/main/cpp/pytorch_nativeapp.cpp:

#include <android/log.h>
#include <cassert>
#include <cmath>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#define ALOGI(...)                                                             \
  __android_log_print(ANDROID_LOG_INFO, "PyTorchNativeApp", __VA_ARGS__)
#define ALOGE(...)                                                             \
  __android_log_print(ANDROID_LOG_ERROR, "PyTorchNativeApp", __VA_ARGS__)

#include "jni.h"

#include <opencv2/opencv.hpp>
#include <torch/script.h>

namespace pytorch_nativeapp {
namespace {
torch::Tensor warp_perspective(torch::Tensor image, torch::Tensor warp) {
  cv::Mat image_mat(/*rows=*/image.size(0),
                    /*cols=*/image.size(1),
                    /*type=*/CV_32FC1,
                    /*data=*/image.data_ptr<float>());
  cv::Mat warp_mat(/*rows=*/warp.size(0),
                   /*cols=*/warp.size(1),
                   /*type=*/CV_32FC1,
                   /*data=*/warp.data_ptr<float>());

  cv::Mat output_mat;
  cv::warpPerspective(image_mat, output_mat, warp_mat, /*dsize=*/{8, 8});

  torch::Tensor output =
      torch::from_blob(output_mat.ptr<float>(), /*sizes=*/{8, 8});
  return output.clone();
}

static auto registry =
    torch::RegisterOperators("my_ops::warp_perspective", &warp_perspective);

template <typename T> void log(const char *m, T t) {
  std::ostringstream os;
  os << t << std::endl;
  ALOGI("%s %s", m, os.str().c_str());
}

struct JITCallGuard {
  torch::autograd::AutoGradMode no_autograd_guard{false};
  torch::AutoNonVariableTypeMode non_var_guard{true};
  torch::jit::GraphOptimizerEnabledGuard no_optimizer_guard{false};
};
} // namespace

static void loadAndForwardModel(JNIEnv *env, jclass, jstring jModelPath) {
  const char *modelPath = env->GetStringUTFChars(jModelPath, 0);
  assert(modelPath);
  JITCallGuard guard;
  torch::jit::Module module = torch::jit::load(modelPath);
  module.eval();
  torch::Tensor x = torch::randn({4, 8});
  torch::Tensor y = torch::randn({8, 5});
  log("x:", x);
  log("y:", y);
  c10::IValue t_out = module.forward({x, y});
  log("result:", t_out);
  env->ReleaseStringUTFChars(jModelPath, modelPath);
}
} // namespace pytorch_nativeapp

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) {
  JNIEnv *env;
  if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
    return JNI_ERR;
  }

  jclass c = env->FindClass("org/pytorch/nativeapp/NativeClient$NativePeer");
  if (c == nullptr) {
    return JNI_ERR;
  }

  static const JNINativeMethod methods[] = {
      {"loadAndForwardModel", "(Ljava/lang/String;)V",
       (void *)pytorch_nativeapp::loadAndForwardModel},
  };
  int rc = env->RegisterNatives(c, methods,
                                sizeof(methods) / sizeof(JNINativeMethod));

  if (rc != JNI_OK) {
    return rc;
  }

  return JNI_VERSION_1_6;
}

This listing is quite long, and a few things intermixed here, we will follow control flow to understand how this code works. The first place where the control flow arrives is JNI_OnLoad. This function is called after loading the library. It is responsible for registering native method, which is called when NativePeer#loadAndForwardModel called, here it is pytorch_nativeapp::loadAndForwardModel function.

pytorch_nativeapp::loadAndForwardModel takes as an argument model path. First, we extract its const char* value and loading the module with torch::jit::load.

To load TorchScript model for mobile, we need to set these guards, because mobile build doesn’t support features like autograd for smaller build size, placed in struct JITCallGuard in this example. It may change in the future. You can track the latest changes keeping an eye on the source in PyTorch GitHub.

Implementation of method warp_perspective and registration of it is entirely the same as in tutorial for desktop build.

Building the app

To specify to gradle where is Android SDK and Android NDK, we need to fill NativeApp/local.properties.

cd NativeApp
echo "sdk.dir=$ANDROID_HOME" >> NativeApp/local.properties
echo "ndk.dir=$ANDROID_NDK" >> NativeApp/local.properties

To build the result apk file we run:

cd NativeApp
gradle app:assembleDebug

To install the app on the connected device:

cd NativeApp
gradle app::installDebug

After that, you can run the app on the device by clicking on PyTorchNativeApp icon. Or you can do it from the command line:

adb shell am start -n org.pytorch.nativeapp/.MainActivity

If you check the android logcat:

adb logcat -v brief | grep PyTorchNativeApp

You should see logs with tag ‘PyTorchNativeApp’ that prints x, y, and the result of the model forward, which we print with log function in NativeApp/app/src/main/cpp/pytorch_nativeapp.cpp.

I/PyTorchNativeApp(26968): x: -0.9484 -1.1757 -0.5832  0.9144  0.8867  1.0933 -0.4004 -0.3389
I/PyTorchNativeApp(26968): -1.0343  1.5200 -0.7625 -1.5724 -1.2073  0.4613  0.2730 -0.6789
I/PyTorchNativeApp(26968): -0.2247 -1.2790  1.0067 -0.9266  0.6034 -0.1941  0.7021 -1.5368
I/PyTorchNativeApp(26968): -0.3803 -0.0188  0.2021 -0.7412 -0.2257  0.5044  0.6592  0.0826
I/PyTorchNativeApp(26968): [ CPUFloatType{4,8} ]
I/PyTorchNativeApp(26968): y: -1.0084  1.8733  0.5435  0.1087 -1.1066
I/PyTorchNativeApp(26968): -1.9926  1.1047  0.5311 -0.4944  1.9178
I/PyTorchNativeApp(26968): -1.5451  0.8867  1.0473 -1.7571  0.3909
I/PyTorchNativeApp(26968):  0.4039  0.5085 -0.2776  0.4080  0.9203
I/PyTorchNativeApp(26968):  0.3655  1.4395 -1.4467 -0.9837  0.3335
I/PyTorchNativeApp(26968): -0.0445  0.8039 -0.2512 -1.3122  0.6543
I/PyTorchNativeApp(26968): -1.5819  0.0525  1.5680 -0.6442 -1.3090
I/PyTorchNativeApp(26968): -1.6197 -0.0773 -0.5967 -0.1105 -0.3122
I/PyTorchNativeApp(26968): [ CPUFloatType{8,5} ]
I/PyTorchNativeApp(26968): result:  16.0274   9.0330   6.0124   9.8644  11.0493
I/PyTorchNativeApp(26968):   8.7633   6.9657  12.3469  10.3159  12.0683
I/PyTorchNativeApp(26968):  12.4529   9.4559  11.7038   7.8396   6.9716
I/PyTorchNativeApp(26968):   8.5279   9.1780  11.3849   8.4368   9.1480
I/PyTorchNativeApp(26968):  10.0000  10.0000  10.0000  10.0000  10.0000
I/PyTorchNativeApp(26968):  10.0000  10.0000  10.0000  10.0000  10.0000
I/PyTorchNativeApp(26968):  10.0000  10.0000  10.0000  10.0000  10.0000
I/PyTorchNativeApp(26968):  10.0000  10.0000  10.0000  10.0000  10.0000
I/PyTorchNativeApp(26968): [ CPUFloatType{8,5} ]

The full setup of this app you can find in PyTorch Android Demo Application Repository.

Docs

Access comprehensive developer documentation for PyTorch

View Docs

Tutorials

Get in-depth tutorials for beginners and advanced developers

View Tutorials

Resources

Find development resources and get your questions answered

View Resources