From 9c9ab9b83835c0e69d3d5651596a008af68e212f Mon Sep 17 00:00:00 2001 From: Hansong Zhang Date: Wed, 23 Apr 2025 00:30:14 -0700 Subject: [PATCH 1/2] Android test use kotlin --- extension/android/build.gradle | 2 +- .../android/executorch_android/build.gradle | 7 +- .../LlmModuleInstrumentationTest.java | 126 ------------ .../LlmModuleInstrumentationTest.kt | 115 +++++++++++ .../org/pytorch/executorch/ModuleE2ETest.java | 106 ---------- .../org/pytorch/executorch/ModuleE2ETest.kt | 97 +++++++++ .../executorch/ModuleInstrumentationTest.java | 189 ------------------ .../executorch/ModuleInstrumentationTest.kt | 188 +++++++++++++++++ .../pytorch/executorch/TensorImageUtils.java | 150 -------------- .../pytorch/executorch/TensorImageUtils.kt | 139 +++++++++++++ extension/android/gradle/libs.versions.toml | 5 + extension/android/settings.gradle | 2 +- 12 files changed, 552 insertions(+), 574 deletions(-) delete mode 100644 extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.java create mode 100644 extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.kt delete mode 100644 extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleE2ETest.java create mode 100644 extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleE2ETest.kt delete mode 100644 extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.java create mode 100644 extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.kt delete mode 100644 extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/TensorImageUtils.java create mode 100644 extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/TensorImageUtils.kt diff --git a/extension/android/build.gradle b/extension/android/build.gradle index ac031653a7a..7b26f8b7f30 100644 --- a/extension/android/build.gradle +++ b/extension/android/build.gradle @@ -16,7 +16,7 @@ allprojects { } dependencies { - classpath 'com.android.tools.build:gradle:8.9.0' + classpath 'com.android.tools.build:gradle:8.5.0' classpath 'com.vanniktech:gradle-maven-publish-plugin:0.31.0' } diff --git a/extension/android/executorch_android/build.gradle b/extension/android/executorch_android/build.gradle index 15088f4097f..eac96936326 100644 --- a/extension/android/executorch_android/build.gradle +++ b/extension/android/executorch_android/build.gradle @@ -7,8 +7,9 @@ */ plugins { - id "com.android.library" version "8.9.0" + id "com.android.library" version "8.5.0" id "com.vanniktech.maven.publish" version "0.31.0" + alias(libs.plugins.jetbrains.kotlin.android) } android { @@ -34,6 +35,9 @@ android { resources.srcDirs += [ 'src/androidTest/resources' ] } } + kotlinOptions { + jvmTarget = '1.8' + } } task copyTestRes(type: Exec) { @@ -43,6 +47,7 @@ task copyTestRes(type: Exec) { dependencies { implementation 'com.facebook.fbjni:fbjni:0.5.1' implementation 'com.facebook.soloader:nativeloader:0.10.5' + implementation libs.core.ktx testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test:rules:1.2.0' diff --git a/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.java b/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.java deleted file mode 100644 index c0a43b25a98..00000000000 --- a/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -package org.pytorch.executorch; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.fail; - -import android.os.Environment; -import androidx.test.rule.GrantPermissionRule; -import android.Manifest; -import android.content.Context; -import org.junit.Test; -import org.junit.Before; -import org.junit.Rule; -import org.junit.runner.RunWith; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; -import java.util.ArrayList; -import java.io.IOException; -import java.io.File; -import java.io.FileOutputStream; -import org.junit.runners.JUnit4; -import org.apache.commons.io.FileUtils; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.InstrumentationRegistry; -import org.json.JSONException; -import org.json.JSONObject; -import org.pytorch.executorch.extension.llm.LlmCallback; -import org.pytorch.executorch.extension.llm.LlmModule; - -/** Unit tests for {@link org.pytorch.executorch.extension.llm.LlmModule}. */ -@RunWith(AndroidJUnit4.class) -public class LlmModuleInstrumentationTest implements LlmCallback { - private static String TEST_FILE_NAME = "/stories.pte"; - private static String TOKENIZER_FILE_NAME = "/tokenizer.bin"; - private static String TEST_PROMPT = "Hello"; - private static int OK = 0x00; - private static int SEQ_LEN = 32; - - private final List results = new ArrayList<>(); - private final List tokensPerSecond = new ArrayList<>(); - private LlmModule mModule; - - private static String getTestFilePath(String fileName) { - return InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalCacheDir() + fileName; - } - - @Before - public void setUp() throws IOException { - // copy zipped test resources to local device - File addPteFile = new File(getTestFilePath(TEST_FILE_NAME)); - InputStream inputStream = getClass().getResourceAsStream(TEST_FILE_NAME); - FileUtils.copyInputStreamToFile(inputStream, addPteFile); - inputStream.close(); - - File tokenizerFile = new File(getTestFilePath(TOKENIZER_FILE_NAME)); - inputStream = getClass().getResourceAsStream(TOKENIZER_FILE_NAME); - FileUtils.copyInputStreamToFile(inputStream, tokenizerFile); - inputStream.close(); - - mModule = new LlmModule(getTestFilePath(TEST_FILE_NAME), getTestFilePath(TOKENIZER_FILE_NAME), 0.0f); - } - - @Rule - public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE); - - @Test - public void testGenerate() throws IOException, URISyntaxException{ - int loadResult = mModule.load(); - // Check that the model can be load successfully - assertEquals(OK, loadResult); - - mModule.generate(TEST_PROMPT, SEQ_LEN, LlmModuleInstrumentationTest.this); - assertEquals(results.size(), SEQ_LEN); - assertTrue(tokensPerSecond.get(tokensPerSecond.size() - 1) > 0); - } - - @Test - public void testGenerateAndStop() throws IOException, URISyntaxException{ - mModule.generate(TEST_PROMPT, SEQ_LEN, new LlmCallback() { - @Override - public void onResult(String result) { - LlmModuleInstrumentationTest.this.onResult(result); - mModule.stop(); - } - - @Override - public void onStats(String stats) { - LlmModuleInstrumentationTest.this.onStats(stats); - } - }); - - int stoppedResultSize = results.size(); - assertTrue(stoppedResultSize < SEQ_LEN); - } - - @Override - public void onResult(String result) { - results.add(result); - } - - @Override - public void onStats(String stats) { - float tps = 0; - try { - JSONObject jsonObject = new JSONObject(stats); - int numGeneratedTokens = jsonObject.getInt("generated_tokens"); - int inferenceEndMs = jsonObject.getInt("inference_end_ms"); - int promptEvalEndMs = jsonObject.getInt("prompt_eval_end_ms"); - tps = (float) numGeneratedTokens / (inferenceEndMs - promptEvalEndMs) * 1000; - tokensPerSecond.add(tps); - } catch (JSONException e) { - } - } -} diff --git a/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.kt b/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.kt new file mode 100644 index 00000000000..199bfc1a4cd --- /dev/null +++ b/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +package org.pytorch.executorch + +import android.Manifest +import androidx.test.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule +import org.apache.commons.io.FileUtils +import org.json.JSONException +import org.json.JSONObject +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.pytorch.executorch.extension.llm.LlmCallback +import org.pytorch.executorch.extension.llm.LlmModule +import java.io.File +import java.io.IOException +import java.net.URISyntaxException + +/** Unit tests for [org.pytorch.executorch.extension.llm.LlmModule]. */ +@RunWith(AndroidJUnit4::class) +class LlmModuleInstrumentationTest : LlmCallback { + private val results: MutableList = ArrayList() + private val tokensPerSecond: MutableList = ArrayList() + private var mModule: LlmModule? = null + + @Before + @Throws(IOException::class) + fun setUp() { + // copy zipped test resources to local device + val addPteFile = File(getTestFilePath(TEST_FILE_NAME)) + var inputStream = javaClass.getResourceAsStream(TEST_FILE_NAME) + FileUtils.copyInputStreamToFile(inputStream, addPteFile) + inputStream.close() + + val tokenizerFile = File(getTestFilePath(TOKENIZER_FILE_NAME)) + inputStream = javaClass.getResourceAsStream(TOKENIZER_FILE_NAME) + FileUtils.copyInputStreamToFile(inputStream, tokenizerFile) + inputStream.close() + + mModule = + LlmModule(getTestFilePath(TEST_FILE_NAME), getTestFilePath(TOKENIZER_FILE_NAME), 0.0f) + } + + @Rule + var mRuntimePermissionRule: GrantPermissionRule = + GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE) + + @Test + @Throws(IOException::class, URISyntaxException::class) + fun testGenerate() { + val loadResult = mModule!!.load() + // Check that the model can be load successfully + Assert.assertEquals(OK.toLong(), loadResult.toLong()) + + mModule!!.generate(TEST_PROMPT, SEQ_LEN, this@LlmModuleInstrumentationTest) + Assert.assertEquals(results.size.toLong(), SEQ_LEN.toLong()) + Assert.assertTrue(tokensPerSecond[tokensPerSecond.size - 1] > 0) + } + + @Test + @Throws(IOException::class, URISyntaxException::class) + fun testGenerateAndStop() { + mModule!!.generate(TEST_PROMPT, SEQ_LEN, object : LlmCallback { + override fun onResult(result: String) { + this@LlmModuleInstrumentationTest.onResult(result) + mModule!!.stop() + } + + override fun onStats(stats: String) { + this@LlmModuleInstrumentationTest.onStats(stats) + } + }) + + val stoppedResultSize = results.size + Assert.assertTrue(stoppedResultSize < SEQ_LEN) + } + + override fun onResult(result: String) { + results.add(result) + } + + override fun onStats(stats: String) { + var tps = 0f + try { + val jsonObject = JSONObject(stats) + val numGeneratedTokens = jsonObject.getInt("generated_tokens") + val inferenceEndMs = jsonObject.getInt("inference_end_ms") + val promptEvalEndMs = jsonObject.getInt("prompt_eval_end_ms") + tps = numGeneratedTokens.toFloat() / (inferenceEndMs - promptEvalEndMs) * 1000 + tokensPerSecond.add(tps) + } catch (e: JSONException) { + } + } + + companion object { + private const val TEST_FILE_NAME = "/stories.pte" + private const val TOKENIZER_FILE_NAME = "/tokenizer.bin" + private const val TEST_PROMPT = "Hello" + private const val OK = 0x00 + private const val SEQ_LEN = 32 + + private fun getTestFilePath(fileName: String): String { + return InstrumentationRegistry.getInstrumentation().targetContext.externalCacheDir.toString() + fileName + } + } +} diff --git a/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleE2ETest.java b/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleE2ETest.java deleted file mode 100644 index 3a033851be9..00000000000 --- a/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleE2ETest.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -package org.pytorch.executorch; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.fail; - -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.Environment; -import androidx.test.rule.GrantPermissionRule; -import android.Manifest; -import android.content.Context; -import org.junit.Test; -import org.junit.Before; -import org.junit.Rule; -import org.junit.runner.RunWith; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicInteger; -import java.io.IOException; -import java.io.File; -import java.io.FileOutputStream; -import org.junit.runners.JUnit4; -import org.apache.commons.io.FileUtils; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.InstrumentationRegistry; - -/** Unit tests for {@link Module}. */ -@RunWith(AndroidJUnit4.class) -public class ModuleE2ETest { - private static String getTestFilePath(String fileName) { - return InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalCacheDir() + fileName; - } - - @Rule - public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE); - - static int argmax(float[] array) { - if (array.length == 0) { - throw new IllegalArgumentException("Array cannot be empty"); - } - int maxIndex = 0; - float maxValue = array[0]; - for (int i = 1; i < array.length; i++) { - if (array[i] > maxValue) { - maxValue = array[i]; - maxIndex = i; - } - } - return maxIndex; - } - - public void testClassification(String filePath) throws IOException, URISyntaxException { - File pteFile = new File(getTestFilePath(filePath)); - InputStream inputStream = getClass().getResourceAsStream(filePath); - FileUtils.copyInputStreamToFile(inputStream, pteFile); - inputStream.close(); - - InputStream imgInputStream = getClass().getResourceAsStream("/banana.jpeg"); - Bitmap bitmap = BitmapFactory.decodeStream(imgInputStream); - bitmap = Bitmap.createScaledBitmap(bitmap, 224, 224, true); - imgInputStream.close(); - - Tensor inputTensor = - TensorImageUtils.bitmapToFloat32Tensor( - bitmap, - TensorImageUtils.TORCHVISION_NORM_MEAN_RGB, - TensorImageUtils.TORCHVISION_NORM_STD_RGB); - - Module module = Module.load(getTestFilePath(filePath)); - - EValue[] results = module.forward(EValue.from(inputTensor)); - assertTrue(results[0].isTensor()); - float[] scores = results[0].toTensor().getDataAsFloatArray(); - - int bananaClass = 954; // From ImageNet 1K - assertEquals(bananaClass, argmax(scores)); - } - - @Test - public void testMv2Fp32() throws IOException, URISyntaxException { - testClassification("/mv2_xnnpack_fp32.pte"); - } - - @Test - public void testMv3Fp32() throws IOException, URISyntaxException { - testClassification("/mv3_xnnpack_fp32.pte"); - } - - @Test - public void testResnet50() throws IOException, URISyntaxException { - testClassification("/resnet50_xnnpack_q8.pte"); - } -} diff --git a/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleE2ETest.kt b/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleE2ETest.kt new file mode 100644 index 00000000000..9a539d020ef --- /dev/null +++ b/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleE2ETest.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +package org.pytorch.executorch + +import android.Manifest +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.test.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule +import org.apache.commons.io.FileUtils +import org.junit.Assert +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File +import java.io.IOException +import java.net.URISyntaxException + +/** Unit tests for [Module]. */ +@RunWith(AndroidJUnit4::class) +class ModuleE2ETest { + @Rule + var mRuntimePermissionRule: GrantPermissionRule = + GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE) + + @Throws(IOException::class, URISyntaxException::class) + fun testClassification(filePath: String) { + val pteFile = File(getTestFilePath(filePath)) + val inputStream = javaClass.getResourceAsStream(filePath) + FileUtils.copyInputStreamToFile(inputStream, pteFile) + inputStream.close() + + val imgInputStream = javaClass.getResourceAsStream("/banana.jpeg") + var bitmap = BitmapFactory.decodeStream(imgInputStream) + bitmap = Bitmap.createScaledBitmap(bitmap!!, 224, 224, true) + imgInputStream.close() + + val inputTensor = + TensorImageUtils.bitmapToFloat32Tensor( + bitmap, + TensorImageUtils.TORCHVISION_NORM_MEAN_RGB, + TensorImageUtils.TORCHVISION_NORM_STD_RGB + ) + + val module = Module.load(getTestFilePath(filePath)) + + val results = module.forward(EValue.from(inputTensor)) + Assert.assertTrue(results[0].isTensor) + val scores = results[0].toTensor().dataAsFloatArray + + val bananaClass = 954 // From ImageNet 1K + Assert.assertEquals(bananaClass.toLong(), argmax(scores).toLong()) + } + + @Test + @Throws(IOException::class, URISyntaxException::class) + fun testMv2Fp32() { + testClassification("/mv2_xnnpack_fp32.pte") + } + + @Test + @Throws(IOException::class, URISyntaxException::class) + fun testMv3Fp32() { + testClassification("/mv3_xnnpack_fp32.pte") + } + + @Test + @Throws(IOException::class, URISyntaxException::class) + fun testResnet50() { + testClassification("/resnet50_xnnpack_q8.pte") + } + + companion object { + private fun getTestFilePath(fileName: String): String { + return InstrumentationRegistry.getInstrumentation().targetContext.externalCacheDir.toString() + fileName + } + + fun argmax(array: FloatArray): Int { + require(array.size != 0) { "Array cannot be empty" } + var maxIndex = 0 + var maxValue = array[0] + for (i in 1 until array.size) { + if (array[i] > maxValue) { + maxValue = array[i] + maxIndex = i + } + } + return maxIndex + } + } +} diff --git a/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.java b/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.java deleted file mode 100644 index f71351ae6ae..00000000000 --- a/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -package org.pytorch.executorch; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.fail; - -import android.os.Environment; -import androidx.test.rule.GrantPermissionRule; -import android.Manifest; -import android.content.Context; -import org.junit.Test; -import org.junit.Before; -import org.junit.Rule; -import org.junit.runner.RunWith; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicInteger; -import java.io.IOException; -import java.io.File; -import java.io.FileOutputStream; -import org.junit.runners.JUnit4; -import org.apache.commons.io.FileUtils; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.InstrumentationRegistry; - -/** Unit tests for {@link Module}. */ -@RunWith(AndroidJUnit4.class) -public class ModuleInstrumentationTest { - private static String TEST_FILE_NAME = "/add.pte"; - private static String MISSING_FILE_NAME = "/missing.pte"; - private static String NON_PTE_FILE_NAME = "/test.txt"; - private static String FORWARD_METHOD = "forward"; - private static String NONE_METHOD = "none"; - private static int OK = 0x00; - private static int INVALID_STATE = 0x2; - private static int INVALID_ARGUMENT = 0x12; - private static int ACCESS_FAILED = 0x22; - - private static String getTestFilePath(String fileName) { - return InstrumentationRegistry.getInstrumentation().getTargetContext().getExternalCacheDir() + fileName; - } - - @Before - public void setUp() throws IOException { - // copy zipped test resources to local device - File addPteFile = new File(getTestFilePath(TEST_FILE_NAME)); - InputStream inputStream = getClass().getResourceAsStream(TEST_FILE_NAME); - FileUtils.copyInputStreamToFile(inputStream, addPteFile); - inputStream.close(); - - File nonPteFile = new File(getTestFilePath(NON_PTE_FILE_NAME)); - inputStream = getClass().getResourceAsStream(NON_PTE_FILE_NAME); - FileUtils.copyInputStreamToFile(inputStream, nonPteFile); - inputStream.close(); - } - - @Rule - public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE); - - @Test - public void testModuleLoadAndForward() throws IOException, URISyntaxException{ - Module module = Module.load(getTestFilePath(TEST_FILE_NAME)); - - EValue[] results = module.forward(); - assertTrue(results[0].isTensor()); - } - - @Test - public void testModuleLoadMethodAndForward() throws IOException{ - Module module = Module.load(getTestFilePath(TEST_FILE_NAME)); - - int loadMethod = module.loadMethod(FORWARD_METHOD); - assertEquals(loadMethod, OK); - - EValue[] results = module.forward(); - assertTrue(results[0].isTensor()); - } - - @Test - public void testModuleLoadForwardExplicit() throws IOException{ - Module module = Module.load(getTestFilePath(TEST_FILE_NAME)); - - EValue[] results = module.execute(FORWARD_METHOD); - assertTrue(results[0].isTensor()); - } - - @Test - public void testModuleLoadNonExistantFile() throws IOException{ - Module module = Module.load(getTestFilePath(MISSING_FILE_NAME)); - - EValue[] results = module.forward(); - assertEquals(null, results); - } - - @Test - public void testModuleLoadMethodNonExistantFile() throws IOException{ - Module module = Module.load(getTestFilePath(MISSING_FILE_NAME)); - - int loadMethod = module.loadMethod(FORWARD_METHOD); - assertEquals(loadMethod, ACCESS_FAILED); - } - - @Test - public void testModuleLoadMethodNonExistantMethod() throws IOException{ - Module module = Module.load(getTestFilePath(TEST_FILE_NAME)); - - int loadMethod = module.loadMethod(NONE_METHOD); - assertEquals(loadMethod, INVALID_ARGUMENT); - } - - @Test - public void testNonPteFile() throws IOException{ - Module module = Module.load(getTestFilePath(NON_PTE_FILE_NAME)); - - int loadMethod = module.loadMethod(FORWARD_METHOD); - assertEquals(loadMethod, INVALID_ARGUMENT); - } - - @Test - public void testLoadOnDestroyedModule() throws IOException{ - Module module = Module.load(getTestFilePath(TEST_FILE_NAME)); - - module.destroy(); - - int loadMethod = module.loadMethod(FORWARD_METHOD); - assertEquals(loadMethod, INVALID_STATE); - } - - @Test - public void testForwardOnDestroyedModule() throws IOException{ - Module module = Module.load(getTestFilePath(TEST_FILE_NAME)); - - int loadMethod = module.loadMethod(FORWARD_METHOD); - assertEquals(loadMethod, OK); - - module.destroy(); - - EValue[] results = module.forward(); - assertEquals(0, results.length); - } - - @Test - public void testForwardFromMultipleThreads() throws InterruptedException, IOException { - Module module = Module.load(getTestFilePath(TEST_FILE_NAME)); - - int numThreads = 100; - CountDownLatch latch = new CountDownLatch(numThreads); - AtomicInteger completed = new AtomicInteger(0); - - Runnable runnable = new Runnable() { - @Override - public void run() { - try { - latch.countDown(); - latch.await(5000, java.util.concurrent.TimeUnit.MILLISECONDS); - EValue[] results = module.forward(); - assertTrue(results[0].isTensor()); - completed.incrementAndGet(); - } catch (InterruptedException e) { - - } - } - }; - - Thread[] threads = new Thread[numThreads]; - for (int i = 0; i < numThreads; i++) { - threads[i] = new Thread(runnable); - threads[i].start(); - } - - for (int i = 0; i < numThreads; i++) { - threads[i].join(); - } - - assertEquals(numThreads, completed.get()); - } -} diff --git a/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.kt b/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.kt new file mode 100644 index 00000000000..92127f70669 --- /dev/null +++ b/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +package org.pytorch.executorch + +import android.Manifest +import androidx.test.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule +import org.apache.commons.io.FileUtils +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File +import java.io.IOException +import java.net.URISyntaxException +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +/** Unit tests for [Module]. */ +@RunWith(AndroidJUnit4::class) +class ModuleInstrumentationTest { + @Before + @Throws(IOException::class) + fun setUp() { + // copy zipped test resources to local device + val addPteFile = File(getTestFilePath(TEST_FILE_NAME)) + var inputStream = javaClass.getResourceAsStream(TEST_FILE_NAME) + FileUtils.copyInputStreamToFile(inputStream, addPteFile) + inputStream.close() + + val nonPteFile = File(getTestFilePath(NON_PTE_FILE_NAME)) + inputStream = javaClass.getResourceAsStream(NON_PTE_FILE_NAME) + FileUtils.copyInputStreamToFile(inputStream, nonPteFile) + inputStream.close() + } + + @Rule + var mRuntimePermissionRule: GrantPermissionRule = + GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE) + + @Test + @Throws(IOException::class, URISyntaxException::class) + fun testModuleLoadAndForward() { + val module = Module.load(getTestFilePath(TEST_FILE_NAME)) + + val results = module.forward() + Assert.assertTrue(results[0].isTensor) + } + + @Test + @Throws(IOException::class) + fun testModuleLoadMethodAndForward() { + val module = Module.load(getTestFilePath(TEST_FILE_NAME)) + + val loadMethod = module.loadMethod(FORWARD_METHOD) + Assert.assertEquals(loadMethod.toLong(), OK.toLong()) + + val results = module.forward() + Assert.assertTrue(results[0].isTensor) + } + + @Test + @Throws(IOException::class) + fun testModuleLoadForwardExplicit() { + val module = Module.load(getTestFilePath(TEST_FILE_NAME)) + + val results = module.execute(FORWARD_METHOD) + Assert.assertTrue(results[0].isTensor) + } + + @Test + @Throws(IOException::class) + fun testModuleLoadNonExistantFile() { + val module = Module.load(getTestFilePath(MISSING_FILE_NAME)) + + val results = module.forward() + Assert.assertEquals(null, results) + } + + @Test + @Throws(IOException::class) + fun testModuleLoadMethodNonExistantFile() { + val module = Module.load(getTestFilePath(MISSING_FILE_NAME)) + + val loadMethod = module.loadMethod(FORWARD_METHOD) + Assert.assertEquals(loadMethod.toLong(), ACCESS_FAILED.toLong()) + } + + @Test + @Throws(IOException::class) + fun testModuleLoadMethodNonExistantMethod() { + val module = Module.load(getTestFilePath(TEST_FILE_NAME)) + + val loadMethod = module.loadMethod(NONE_METHOD) + Assert.assertEquals(loadMethod.toLong(), INVALID_ARGUMENT.toLong()) + } + + @Test + @Throws(IOException::class) + fun testNonPteFile() { + val module = Module.load(getTestFilePath(NON_PTE_FILE_NAME)) + + val loadMethod = module.loadMethod(FORWARD_METHOD) + Assert.assertEquals(loadMethod.toLong(), INVALID_ARGUMENT.toLong()) + } + + @Test + @Throws(IOException::class) + fun testLoadOnDestroyedModule() { + val module = Module.load(getTestFilePath(TEST_FILE_NAME)) + + module.destroy() + + val loadMethod = module.loadMethod(FORWARD_METHOD) + Assert.assertEquals(loadMethod.toLong(), INVALID_STATE.toLong()) + } + + @Test + @Throws(IOException::class) + fun testForwardOnDestroyedModule() { + val module = Module.load(getTestFilePath(TEST_FILE_NAME)) + + val loadMethod = module.loadMethod(FORWARD_METHOD) + Assert.assertEquals(loadMethod.toLong(), OK.toLong()) + + module.destroy() + + val results = module.forward() + Assert.assertEquals(0, results.size.toLong()) + } + + @Test + @Throws(InterruptedException::class, IOException::class) + fun testForwardFromMultipleThreads() { + val module = Module.load(getTestFilePath(TEST_FILE_NAME)) + + val numThreads = 100 + val latch = CountDownLatch(numThreads) + val completed = AtomicInteger(0) + + val runnable = Runnable { + try { + latch.countDown() + latch.await(5000, TimeUnit.MILLISECONDS) + val results = module.forward() + Assert.assertTrue(results[0].isTensor) + completed.incrementAndGet() + } catch (e: InterruptedException) { + } + } + + val threads = arrayOfNulls(numThreads) + for (i in 0 until numThreads) { + threads[i] = Thread(runnable) + threads[i]!!.start() + } + + for (i in 0 until numThreads) { + threads[i]!!.join() + } + + Assert.assertEquals(numThreads.toLong(), completed.get().toLong()) + } + + companion object { + private const val TEST_FILE_NAME = "/add.pte" + private const val MISSING_FILE_NAME = "/missing.pte" + private const val NON_PTE_FILE_NAME = "/test.txt" + private const val FORWARD_METHOD = "forward" + private const val NONE_METHOD = "none" + private const val OK = 0x00 + private const val INVALID_STATE = 0x2 + private const val INVALID_ARGUMENT = 0x12 + private const val ACCESS_FAILED = 0x22 + + private fun getTestFilePath(fileName: String): String { + return InstrumentationRegistry.getInstrumentation().targetContext.externalCacheDir.toString() + fileName + } + } +} diff --git a/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/TensorImageUtils.java b/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/TensorImageUtils.java deleted file mode 100644 index 95434dcb734..00000000000 --- a/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/TensorImageUtils.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -package org.pytorch.executorch; - -import android.graphics.Bitmap; -import android.util.Log; -import java.nio.FloatBuffer; -import org.pytorch.executorch.Tensor; - -/** - * Contains utility functions for {@link Tensor} creation from {@link android.graphics.Bitmap} or - * {@link android.media.Image} source. - */ -public final class TensorImageUtils { - - public static float[] TORCHVISION_NORM_MEAN_RGB = new float[] {0.485f, 0.456f, 0.406f}; - public static float[] TORCHVISION_NORM_STD_RGB = new float[] {0.229f, 0.224f, 0.225f}; - - /** - * Creates new {@link Tensor} from full {@link android.graphics.Bitmap}, normalized with specified - * in parameters mean and std. - * - * @param normMeanRGB means for RGB channels normalization, length must equal 3, RGB order - * @param normStdRGB standard deviation for RGB channels normalization, length must equal 3, RGB - * order - */ - public static Tensor bitmapToFloat32Tensor( - final Bitmap bitmap, final float[] normMeanRGB, final float normStdRGB[]) { - checkNormMeanArg(normMeanRGB); - checkNormStdArg(normStdRGB); - - return bitmapToFloat32Tensor( - bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), normMeanRGB, normStdRGB); - } - - /** - * Writes tensor content from specified {@link android.graphics.Bitmap}, normalized with specified - * in parameters mean and std to specified {@link java.nio.FloatBuffer} with specified offset. - * - * @param bitmap {@link android.graphics.Bitmap} as a source for Tensor data - * @param x - x coordinate of top left corner of bitmap's area - * @param y - y coordinate of top left corner of bitmap's area - * @param width - width of bitmap's area - * @param height - height of bitmap's area - * @param normMeanRGB means for RGB channels normalization, length must equal 3, RGB order - * @param normStdRGB standard deviation for RGB channels normalization, length must equal 3, RGB - * order - */ - public static void bitmapToFloatBuffer( - final Bitmap bitmap, - final int x, - final int y, - final int width, - final int height, - final float[] normMeanRGB, - final float[] normStdRGB, - final FloatBuffer outBuffer, - final int outBufferOffset) { - checkOutBufferCapacity(outBuffer, outBufferOffset, width, height); - checkNormMeanArg(normMeanRGB); - checkNormStdArg(normStdRGB); - final int pixelsCount = height * width; - final int[] pixels = new int[pixelsCount]; - bitmap.getPixels(pixels, 0, width, x, y, width, height); - final int offset_g = pixelsCount; - final int offset_b = 2 * pixelsCount; - for (int i = 0; i < 100; i++) { - final int c = pixels[i]; - Log.i("Image", ": " + i + " " + ((c >> 16) & 0xff)); - } - for (int i = 0; i < pixelsCount; i++) { - final int c = pixels[i]; - float r = ((c >> 16) & 0xff) / 255.0f; - float g = ((c >> 8) & 0xff) / 255.0f; - float b = ((c) & 0xff) / 255.0f; - outBuffer.put(outBufferOffset + i, (r - normMeanRGB[0]) / normStdRGB[0]); - outBuffer.put(outBufferOffset + offset_g + i, (g - normMeanRGB[1]) / normStdRGB[1]); - outBuffer.put(outBufferOffset + offset_b + i, (b - normMeanRGB[2]) / normStdRGB[2]); - } - } - - /** - * Creates new {@link Tensor} from specified area of {@link android.graphics.Bitmap}, normalized - * with specified in parameters mean and std. - * - * @param bitmap {@link android.graphics.Bitmap} as a source for Tensor data - * @param x - x coordinate of top left corner of bitmap's area - * @param y - y coordinate of top left corner of bitmap's area - * @param width - width of bitmap's area - * @param height - height of bitmap's area - * @param normMeanRGB means for RGB channels normalization, length must equal 3, RGB order - * @param normStdRGB standard deviation for RGB channels normalization, length must equal 3, RGB - * order - */ - public static Tensor bitmapToFloat32Tensor( - final Bitmap bitmap, - int x, - int y, - int width, - int height, - float[] normMeanRGB, - float[] normStdRGB) { - checkNormMeanArg(normMeanRGB); - checkNormStdArg(normStdRGB); - - final FloatBuffer floatBuffer = Tensor.allocateFloatBuffer(3 * width * height); - bitmapToFloatBuffer(bitmap, x, y, width, height, normMeanRGB, normStdRGB, floatBuffer, 0); - return Tensor.fromBlob(floatBuffer, new long[] {1, 3, height, width}); - } - - private static void checkOutBufferCapacity( - FloatBuffer outBuffer, int outBufferOffset, int tensorWidth, int tensorHeight) { - if (outBufferOffset + 3 * tensorWidth * tensorHeight > outBuffer.capacity()) { - throw new IllegalStateException("Buffer underflow"); - } - } - - private static void checkTensorSize(int tensorWidth, int tensorHeight) { - if (tensorHeight <= 0 || tensorWidth <= 0) { - throw new IllegalArgumentException("tensorHeight and tensorWidth must be positive"); - } - } - - private static void checkRotateCWDegrees(int rotateCWDegrees) { - if (rotateCWDegrees != 0 - && rotateCWDegrees != 90 - && rotateCWDegrees != 180 - && rotateCWDegrees != 270) { - throw new IllegalArgumentException("rotateCWDegrees must be one of 0, 90, 180, 270"); - } - } - - private static void checkNormStdArg(float[] normStdRGB) { - if (normStdRGB.length != 3) { - throw new IllegalArgumentException("normStdRGB length must be 3"); - } - } - - private static void checkNormMeanArg(float[] normMeanRGB) { - if (normMeanRGB.length != 3) { - throw new IllegalArgumentException("normMeanRGB length must be 3"); - } - } -} diff --git a/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/TensorImageUtils.kt b/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/TensorImageUtils.kt new file mode 100644 index 00000000000..2d082f1efc5 --- /dev/null +++ b/extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/TensorImageUtils.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +package org.pytorch.executorch + +import android.graphics.Bitmap +import android.util.Log +import java.nio.FloatBuffer + +/** + * Contains utility functions for [Tensor] creation from [android.graphics.Bitmap] or + * [android.media.Image] source. + */ +object TensorImageUtils { + var TORCHVISION_NORM_MEAN_RGB: FloatArray = floatArrayOf(0.485f, 0.456f, 0.406f) + var TORCHVISION_NORM_STD_RGB: FloatArray = floatArrayOf(0.229f, 0.224f, 0.225f) + + /** + * Creates new [Tensor] from full [android.graphics.Bitmap], normalized with specified + * in parameters mean and std. + * + * @param normMeanRGB means for RGB channels normalization, length must equal 3, RGB order + * @param normStdRGB standard deviation for RGB channels normalization, length must equal 3, RGB + * order + */ + fun bitmapToFloat32Tensor( + bitmap: Bitmap, normMeanRGB: FloatArray, normStdRGB: FloatArray + ): Tensor { + checkNormMeanArg(normMeanRGB) + checkNormStdArg(normStdRGB) + + return bitmapToFloat32Tensor( + bitmap, 0, 0, bitmap.width, bitmap.height, normMeanRGB, normStdRGB + ) + } + + /** + * Writes tensor content from specified [android.graphics.Bitmap], normalized with specified + * in parameters mean and std to specified [java.nio.FloatBuffer] with specified offset. + * + * @param bitmap [android.graphics.Bitmap] as a source for Tensor data + * @param x - x coordinate of top left corner of bitmap's area + * @param y - y coordinate of top left corner of bitmap's area + * @param width - width of bitmap's area + * @param height - height of bitmap's area + * @param normMeanRGB means for RGB channels normalization, length must equal 3, RGB order + * @param normStdRGB standard deviation for RGB channels normalization, length must equal 3, RGB + * order + */ + fun bitmapToFloatBuffer( + bitmap: Bitmap, + x: Int, + y: Int, + width: Int, + height: Int, + normMeanRGB: FloatArray, + normStdRGB: FloatArray, + outBuffer: FloatBuffer, + outBufferOffset: Int + ) { + checkOutBufferCapacity(outBuffer, outBufferOffset, width, height) + checkNormMeanArg(normMeanRGB) + checkNormStdArg(normStdRGB) + val pixelsCount = height * width + val pixels = IntArray(pixelsCount) + bitmap.getPixels(pixels, 0, width, x, y, width, height) + val offset_g = pixelsCount + val offset_b = 2 * pixelsCount + for (i in 0..99) { + val c = pixels[i] + Log.i("Image", ": " + i + " " + ((c shr 16) and 0xff)) + } + for (i in 0 until pixelsCount) { + val c = pixels[i] + val r = ((c shr 16) and 0xff) / 255.0f + val g = ((c shr 8) and 0xff) / 255.0f + val b = ((c) and 0xff) / 255.0f + outBuffer.put(outBufferOffset + i, (r - normMeanRGB[0]) / normStdRGB[0]) + outBuffer.put(outBufferOffset + offset_g + i, (g - normMeanRGB[1]) / normStdRGB[1]) + outBuffer.put(outBufferOffset + offset_b + i, (b - normMeanRGB[2]) / normStdRGB[2]) + } + } + + /** + * Creates new [Tensor] from specified area of [android.graphics.Bitmap], normalized + * with specified in parameters mean and std. + * + * @param bitmap [android.graphics.Bitmap] as a source for Tensor data + * @param x - x coordinate of top left corner of bitmap's area + * @param y - y coordinate of top left corner of bitmap's area + * @param width - width of bitmap's area + * @param height - height of bitmap's area + * @param normMeanRGB means for RGB channels normalization, length must equal 3, RGB order + * @param normStdRGB standard deviation for RGB channels normalization, length must equal 3, RGB + * order + */ + fun bitmapToFloat32Tensor( + bitmap: Bitmap, + x: Int, + y: Int, + width: Int, + height: Int, + normMeanRGB: FloatArray, + normStdRGB: FloatArray + ): Tensor { + checkNormMeanArg(normMeanRGB) + checkNormStdArg(normStdRGB) + + val floatBuffer = Tensor.allocateFloatBuffer(3 * width * height) + bitmapToFloatBuffer(bitmap, x, y, width, height, normMeanRGB, normStdRGB, floatBuffer, 0) + return Tensor.fromBlob(floatBuffer, longArrayOf(1, 3, height.toLong(), width.toLong())) + } + + private fun checkOutBufferCapacity( + outBuffer: FloatBuffer, outBufferOffset: Int, tensorWidth: Int, tensorHeight: Int + ) { + check(outBufferOffset + 3 * tensorWidth * tensorHeight <= outBuffer.capacity()) { "Buffer underflow" } + } + + private fun checkTensorSize(tensorWidth: Int, tensorHeight: Int) { + require(!(tensorHeight <= 0 || tensorWidth <= 0)) { "tensorHeight and tensorWidth must be positive" } + } + + private fun checkRotateCWDegrees(rotateCWDegrees: Int) { + require(!(rotateCWDegrees != 0 && rotateCWDegrees != 90 && rotateCWDegrees != 180 && rotateCWDegrees != 270)) { "rotateCWDegrees must be one of 0, 90, 180, 270" } + } + + private fun checkNormStdArg(normStdRGB: FloatArray) { + require(normStdRGB.size == 3) { "normStdRGB length must be 3" } + } + + private fun checkNormMeanArg(normMeanRGB: FloatArray) { + require(normMeanRGB.size == 3) { "normMeanRGB length must be 3" } + } +} diff --git a/extension/android/gradle/libs.versions.toml b/extension/android/gradle/libs.versions.toml index 561988cb1f6..dd2cf3f039a 100644 --- a/extension/android/gradle/libs.versions.toml +++ b/extension/android/gradle/libs.versions.toml @@ -5,8 +5,13 @@ commons-math3 = "3.6.1" guava = "32.1.3-jre" junit = "4.13.2" +core-ktx = "1.13.1" +kotlin = "2.1.20" [libraries] commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "commons-math3" } guava = { module = "com.google.guava:guava", version.ref = "guava" } junit = { module = "junit:junit", version.ref = "junit" } +core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } +[plugins] +jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/extension/android/settings.gradle b/extension/android/settings.gradle index 95d46203058..7bd0e21f602 100644 --- a/extension/android/settings.gradle +++ b/extension/android/settings.gradle @@ -16,7 +16,7 @@ pluginManagement { plugins { // Apply the foojay-resolver plugin to allow automatic download of JDKs - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0' } rootProject.name = 'executorch' From 54e6bcb2a7372a8859d048f360260597502394d9 Mon Sep 17 00:00:00 2001 From: Hansong Zhang Date: Thu, 24 Apr 2025 16:28:43 -0700 Subject: [PATCH 2/2] Update --- extension/android/build.gradle | 2 +- extension/android/executorch_android/build.gradle | 2 +- extension/android/settings.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extension/android/build.gradle b/extension/android/build.gradle index 7b26f8b7f30..ac031653a7a 100644 --- a/extension/android/build.gradle +++ b/extension/android/build.gradle @@ -16,7 +16,7 @@ allprojects { } dependencies { - classpath 'com.android.tools.build:gradle:8.5.0' + classpath 'com.android.tools.build:gradle:8.9.0' classpath 'com.vanniktech:gradle-maven-publish-plugin:0.31.0' } diff --git a/extension/android/executorch_android/build.gradle b/extension/android/executorch_android/build.gradle index eac96936326..6fd07027dda 100644 --- a/extension/android/executorch_android/build.gradle +++ b/extension/android/executorch_android/build.gradle @@ -7,7 +7,7 @@ */ plugins { - id "com.android.library" version "8.5.0" + id "com.android.library" version "8.9.0" id "com.vanniktech.maven.publish" version "0.31.0" alias(libs.plugins.jetbrains.kotlin.android) } diff --git a/extension/android/settings.gradle b/extension/android/settings.gradle index 7bd0e21f602..95d46203058 100644 --- a/extension/android/settings.gradle +++ b/extension/android/settings.gradle @@ -16,7 +16,7 @@ pluginManagement { plugins { // Apply the foojay-resolver plugin to allow automatic download of JDKs - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' } rootProject.name = 'executorch'