From 27f73cb9d41ffce1154136c23107331cd79e605d Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Tue, 11 Apr 2023 16:45:17 +0200 Subject: [PATCH 1/4] Add simulator tests for Android --- .gitignore | 1 + build.gradle.kts | 7 +- .../TorchModuleAndroidTest.kt | 341 ++++++++++++++++++ 3 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 src/androidTest/java/de/voize/pytorch_lite_multiplatform/TorchModuleAndroidTest.kt diff --git a/.gitignore b/.gitignore index 1238b52..efa413c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build/ ios/LibTorchWrapper/LibTorchWrapper.xcworkspace/xcuserdata pytorch_lite_multiplatform.podspec bin/ +src/androidTest/assets/dummy_module.ptl \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 80462ec..b39933c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,7 +59,11 @@ kotlin { } val androidTest by getting { dependencies { - implementation("junit:junit:4.13") + implementation("junit:junit:4.13.2") + implementation("androidx.test:core:1.5.0") + implementation("androidx.test:rules:1.5.0") + implementation("androidx.test:runner:1.5.2") + implementation("androidx.test.ext:junit:1.1.5") } } } @@ -154,6 +158,7 @@ android { defaultConfig { minSdkVersion(24) targetSdkVersion(30) + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { getByName("release") { diff --git a/src/androidTest/java/de/voize/pytorch_lite_multiplatform/TorchModuleAndroidTest.kt b/src/androidTest/java/de/voize/pytorch_lite_multiplatform/TorchModuleAndroidTest.kt new file mode 100644 index 0000000..50453b9 --- /dev/null +++ b/src/androidTest/java/de/voize/pytorch_lite_multiplatform/TorchModuleAndroidTest.kt @@ -0,0 +1,341 @@ +package de.voize.pytorch_lite_multiplatform + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.suparnatural.core.fs.FileSystem +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.* + +@RunWith(AndroidJUnit4::class) +class TorchModuleAndroidTest { + private val assetsManager = InstrumentationRegistry.getInstrumentation().context.assets + private val moduleName = "dummy_module.ptl" + + private val contentsDir = FileSystem.contentsDirectory.absolutePath + private val localModulePathComponent = contentsDir?.byAppending(moduleName)!! + private val localModulePath = localModulePathComponent.component!! + + init { + val inputStream = assetsManager.open(moduleName) + val data = inputStream.readBytes() + val created = FileSystem.writeFile(localModulePathComponent, data, create = true) + inputStream.close() + } + + @Test + fun itCanLoadRawLibTorchModule() { + val module = TorchModule(localModulePath) + assertNotNull(module) + } + + @Test + fun itCanRunMethod() { + plmScoped { + val module = TorchModule(localModulePath) + val inputTensor = Tensor.fromBlob( + FloatArray(10) { 0.0F }, + longArrayOf(1, 10), + this, + ) + val input = IValue.from(inputTensor) + val output = module.runMethod("inference", input) + assertTrue { output.isTensor() } + val outputTensor = output.toTensor() + + assertEquals(10, outputTensor.getDataAsFloatArray().size) + assertContentEquals(longArrayOf(1, 10), outputTensor.shape()) + } + } + + @Test + fun itCanRunForward() { + plmScoped { + val module = TorchModule(localModulePath) + val output = module.forward( + IValue.from( + Tensor.fromBlob( + FloatArray(10) { 0.0F }, + longArrayOf(1, 10), + this, + ) + ) + ) + val outputTensor = output.toTensor() + assertEquals(10, outputTensor.getDataAsFloatArray().size) + assertContentEquals(longArrayOf(1, 10), outputTensor.shape()) + } + } + + @Test + fun testTensorWrapperLong() { + plmScoped { + val data = longArrayOf(3L, 2L, 0L, 0L, 1L, 6L) + val shape = longArrayOf(2, 3) + val tensor = Tensor.fromBlob(data, shape, this) + assertEquals(data.toList(), tensor.getDataAsLongArray().toList()) + assertEquals(shape.toList(), tensor.shape().toList()) + } + } + + @Test + fun testIValueWrapperList() { + plmScoped { + val a = IValue.from(1L) + val b = IValue.from(2L) + val wrapped = IValue.listFrom(a, b) + val asList = wrapped.toList().map { it.toLong() } + assertEquals(asList.first(), 1L) + assertEquals(asList.last(), 2L) + } + } + + @Test + fun testIValueWrapperTensors() { + plmScoped { + val a = longArrayOf(3L, 2L, 0L, 0L, 1L, 6L) + val aShape = longArrayOf(2, 3) + val b = longArrayOf(3L, 2L, 0L) + val bShape = longArrayOf(3) + val l = IValue.listFrom( + Tensor.fromBlob(a, aShape, this), + Tensor.fromBlob(b, bShape, this) + ) + val asList = l.toTensorList().map { it.getDataAsLongArray() } + assertEquals(asList.first().toList(), a.toList()) + assertEquals(asList.last().toList(), b.toList()) + } + } + + @Test + fun testIValueNull() { + plmScoped { + val a = IValue.optionalNull() + val b = IValue.from(0.0) + assertTrue(a.isNull()) + assertTrue(!b.isNull()) + } + } + + @Test + fun testIValueString() { + plmScoped { + val greeting = "hello world from pytorch-lite-multiplatform with▁unicode" + val a = IValue.from(greeting) + assertEquals(greeting, a.toStr()) + } + } + + @Test + fun testIValueStringIdentity() { + plmScoped { + val module = TorchModule(localModulePath) + val a = IValue.from("test") + val output = module.runMethod("identity_string", a) + assertEquals("test", output.toStr()) + } + } + + @Test + fun testIValueDictionaries() { + plmScoped { + val greeting = "hello world from pytorch-lite-multiplatform" + val a = IValue.dictStringKeyFrom(mapOf(greeting to IValue.from(1L))) + assertEquals(1L, a.toDictStringKey()[greeting]?.toLong()) + val b = IValue.dictLongKeyFrom(mapOf(0L to IValue.from(2L))) + assertEquals(2L, b.toDictLongKey()[0L]?.toLong()) + } + } + + @Test + fun testIValueDictionariesIdentity() { + plmScoped { + val module = TorchModule(localModulePath) + val input = IValue.dictLongKeyFrom( + mapOf( + 0L to IValue.from(7L), + ) + ) + val output = module.runMethod( + "identity_dict_long", + input + ) + assertEquals(7L, output.toDictLongKey()[0L]?.toLong()) + } + } + + /* + @Test + fun testIValueWrapperListWrongTypes() { + plmScoped { + val a = IValue.from(1L) + val b = IValue.from(0.0) + assertFailsWith { + IValue.listFrom(a, b) + } + } + } + */ + + @Test + fun testIValueWrapperTuple() { + plmScoped { + val a = IValue.from(1L) + val b = IValue.from(0.123) + val wrapped = IValue.tupleFrom(a, b) + val asList = wrapped.toTuple() + assertEquals(asList.first().toLong(), 1L) + assertEquals(asList.last().toDouble(), 0.123) + } + } + + @Test + fun testIdentityLong() { + plmScoped { + val module = TorchModule(localModulePath) + val input = IValue.from(0L) + val output = module.runMethod( + "identity_long", + input + ) + assertEquals(0L, output.toLong()) + } + } + + @Test + fun testIdentityBool() { + plmScoped { + val module = TorchModule(localModulePath) + val input = IValue.from(false) + val output = module.runMethod( + "identity_bool", + input + ) + assertEquals(false, output.toBool()) + + val input2 = IValue.from(true) + val output2 = module.runMethod( + "identity_bool", + input2 + ) + assertEquals(true, output2.toBool()) + } + } + + @Test + fun testIdentityBoolList() { + plmScoped { + val module = TorchModule(localModulePath) + val input = IValue.listFrom(IValue.from(true), IValue.from(false)) + val output = module.runMethod( + "identity_bool_list", + input + ) + assertEquals(listOf(true, false), output.toBoolList()) + } + } + + @Test + fun testIdentityBoolList2() { + plmScoped { + val module = TorchModule(localModulePath) + val input = IValue.listFrom(true, false, scope = this) + val output = module.runMethod( + "identity_bool_list", + input + ) + assertEquals(listOf(true, false), output.toBoolList()) + } + } + + @Test + fun testIdentityTensor() { + plmScoped { + val module = TorchModule(localModulePath) + val data = floatArrayOf(0.86F, 1.36F, 0.51F, 0.45F, 0.37F, 1.84F) + val shape = longArrayOf(2, 3) + val tensor = Tensor.fromBlob(data, shape, this) + val output = module.runMethod( + "identity_tensor", + IValue.from(tensor) + ) + val outputTensor = output.toTensor() + assertEquals(data.toList(), outputTensor.getDataAsFloatArray().toList()) + assertEquals(shape.toList(), outputTensor.shape().toList()) + } + } + + @Test + fun testSimilarity() { + plmScoped { + val module = TorchModule(localModulePath) + val output = module.runMethod( + "similarity", + IValue.from( + Tensor.fromBlob( + floatArrayOf(0.86F, 1.36F, 0.51F, 0.45F, 0.37F, 1.84F), + longArrayOf(2, 3), + this, + ) + ), + IValue.from( + Tensor.fromBlob( + floatArrayOf(1.02F, 0.17F, 1.99F, 1.02F, 0.82F, 1.33F), + longArrayOf(2, 3), + this, + ) + ) + ) + val outputTensor = output.toTensor() + val data = outputTensor.getDataAsFloatArray() + assertEquals(listOf(2L), outputTensor.shape().toList()) + assertEquals(0.56F, data[0], 0.01F) + assertEquals(0.89F, data[1], 0.01F) + } + } + + @Test + fun itCanRunMethodWithStringDictInput() { + plmScoped { + val module = TorchModule(localModulePath) + val input = IValue.dictStringKeyFrom( + mapOf( + "x" to IValue.from( + Tensor.fromBlob( + FloatArray(10) { 0.0F }, + longArrayOf(1, 10), + this, + ) + ) + ) + ) + val output = module.runMethod("inference_dict_string", input) + val outputTensor = output.toTensor() + assertEquals(10, outputTensor.getDataAsFloatArray().size) + assertContentEquals(longArrayOf(1, 10), outputTensor.shape()) + } + } + + @Test + fun itCanRunMethodWithLongDictInput() { + plmScoped { + val module = TorchModule(localModulePath) + val input = IValue.dictLongKeyFrom( + mapOf( + 0L to IValue.from( + Tensor.fromBlob( + FloatArray(10) { 0.0F }, + longArrayOf(1, 10), + this, + ) + ) + ) + ) + val output = module.runMethod("inference_dict_long", input) + val outputTensor = output.toTensor() + assertEquals(10, outputTensor.getDataAsFloatArray().size) + assertContentEquals(longArrayOf(1, 10), outputTensor.shape()) + } + } +} \ No newline at end of file From ac5a674ac0a06de005df73ea0e403764cfd8284f Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Tue, 11 Apr 2023 16:58:38 +0200 Subject: [PATCH 2/4] Add CI job for Android instrumented tests --- .github/workflows/test.yml | 20 ++++++++++++++++++- .../TorchModuleAndroidTest.kt | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 657d065..c58d4af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,25 @@ jobs: - run: xcrun simctl list devices iOS - run: python3 --version - run: pip3 install torch --extra-index-url https://download.pytorch.org/whl/cpu - - run: ./gradlew iosSimulatorX64Test + - run: ./gradlew + test-android-instrumented: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '11' + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + - run: xcrun simctl list devices iOS + - run: python3 --version + - run: pip3 install torch --extra-index-url https://download.pytorch.org/whl/cpu + - run: python3 build_dummy_model.py + - run: mkdir src/androidTest/assets + - run: mv dummy_module.ptl src/androidTest/assets/ + - run: ./gradlew connectedAndroidTest build: runs-on: macos-latest steps: diff --git a/src/androidTest/java/de/voize/pytorch_lite_multiplatform/TorchModuleAndroidTest.kt b/src/androidTest/java/de/voize/pytorch_lite_multiplatform/TorchModuleAndroidTest.kt index 50453b9..f3993f0 100644 --- a/src/androidTest/java/de/voize/pytorch_lite_multiplatform/TorchModuleAndroidTest.kt +++ b/src/androidTest/java/de/voize/pytorch_lite_multiplatform/TorchModuleAndroidTest.kt @@ -102,7 +102,7 @@ class TorchModuleAndroidTest { Tensor.fromBlob(a, aShape, this), Tensor.fromBlob(b, bShape, this) ) - val asList = l.toTensorList().map { it.getDataAsLongArray() } + val asList = l.toList().map { it.toTensor().getDataAsLongArray() } assertEquals(asList.first().toList(), a.toList()) assertEquals(asList.last().toList(), b.toList()) } From df43f1585943878b7071bf85c123bab5c39d0f06 Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Mon, 18 Mar 2024 17:31:00 +0100 Subject: [PATCH 3/4] Add test for named tuples --- build_dummy_model.py | 13 ++++++++++-- .../TorchModuleAndroidTest.kt | 20 +++++++++++++++++++ .../TorchModuleIOSTest.kt | 20 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/build_dummy_model.py b/build_dummy_model.py index 0fe9362..39e799a 100644 --- a/build_dummy_model.py +++ b/build_dummy_model.py @@ -1,8 +1,13 @@ -from typing import Dict, List, Tuple +from typing import Dict, List, NamedTuple, Tuple import torch import torch.nn as nn +@torch.jit.export +class State(NamedTuple): + a: int + b: int + class DummyModel(nn.Module): def __init__(self): super().__init__() @@ -57,6 +62,10 @@ def identity_dict_long(self, x: Dict[int, int]): def identity_string(self, x: str): return x + @torch.jit.export + def identity_named_tuple(self, x: State): + return x + @torch.jit.export def similarity(self, x: torch.Tensor, y: torch.Tensor): return self.cos_sim(x, y) @@ -74,4 +83,4 @@ def main(): if __name__ == "__main__": print("PyTorch Version", torch.__version__) - main() \ No newline at end of file + main() diff --git a/src/androidTest/java/de/voize/pytorch_lite_multiplatform/TorchModuleAndroidTest.kt b/src/androidTest/java/de/voize/pytorch_lite_multiplatform/TorchModuleAndroidTest.kt index f3993f0..5f40979 100644 --- a/src/androidTest/java/de/voize/pytorch_lite_multiplatform/TorchModuleAndroidTest.kt +++ b/src/androidTest/java/de/voize/pytorch_lite_multiplatform/TorchModuleAndroidTest.kt @@ -266,6 +266,26 @@ class TorchModuleAndroidTest { } } + @Test + fun testIdentityNamedTuple() { + plmScoped { + val module = TorchModule(localModulePath) + + val a = IValue.from(1L) + val b = IValue.from(2L) + val tuple = IValue.tupleFrom(a, b) + + val output = module.runMethod( + "identity_named_tuple", + tuple, + ) + + val (outputA, outputB) = output.toTuple() + assertEquals(outputA.toLong(), a.toLong()) + assertEquals(outputB.toLong(), b.toLong()) + } + } + @Test fun testSimilarity() { plmScoped { diff --git a/src/iosX64Test/kotlin/de/voize/pytorch_lite_multiplatform/TorchModuleIOSTest.kt b/src/iosX64Test/kotlin/de/voize/pytorch_lite_multiplatform/TorchModuleIOSTest.kt index be347e3..978b348 100644 --- a/src/iosX64Test/kotlin/de/voize/pytorch_lite_multiplatform/TorchModuleIOSTest.kt +++ b/src/iosX64Test/kotlin/de/voize/pytorch_lite_multiplatform/TorchModuleIOSTest.kt @@ -251,6 +251,26 @@ class TorchModuleIOSTest { } } + @Test + fun testIdentityNamedTuple() { + plmScoped { + val module = TorchModule(localModulePath) + + val a = IValue.from(1L) + val b = IValue.from(2L) + val tuple = IValue.tupleFrom(a, b) + + val output = module.runMethod( + "identity_named_tuple", + tuple, + ) + + val (outputA, outputB) = output.toTuple() + assertEquals(outputA.toLong(), a.toLong()) + assertEquals(outputB.toLong(), b.toLong()) + } + } + @Test fun testSimilarity() { plmScoped { From 368a9bd3ba8240f8a3cd504132124578cfaf0956 Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Mon, 18 Mar 2024 17:33:44 +0100 Subject: [PATCH 4/4] Fix test-ios-simulator-x64 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c58d4af..11996c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: - run: xcrun simctl list devices iOS - run: python3 --version - run: pip3 install torch --extra-index-url https://download.pytorch.org/whl/cpu - - run: ./gradlew + - run: ./gradlew iosSimulatorX64Test test-android-instrumented: runs-on: macos-latest steps: