From bd1d6859296959532460c2f99cf22188e64f84f7 Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Thu, 23 Apr 2026 15:36:16 -0700 Subject: [PATCH 1/8] Android: consistent error types across all modules TrainingModule: implement Closeable, replace Log.e + silent empty returns with IllegalStateException throws. Add checkNotDestroyed() guard on all public methods. SGD: throw IllegalStateException instead of bare RuntimeException when optimizer is destroyed. AsrModule: throw ExecutorchRuntimeException instead of bare RuntimeException on transcription failure. ExecuTorchRuntime.validateFilePath: throw IllegalArgumentException instead of bare RuntimeException, with descriptive message. JNI constructors: wrap ExecuTorchJni and ExecuTorchLlmJni constructor bodies in try-catch so C++ exceptions become ExecutorchRuntimeException instead of generic RuntimeException. This commit was authored with the help of Claude. --- .../pytorch/executorch/ExecuTorchRuntime.java | 3 +- .../executorch/extension/asr/AsrModule.kt | 2 +- .../org/pytorch/executorch/training/SGD.java | 2 +- .../executorch/training/TrainingModule.java | 32 ++++++++++--------- extension/android/jni/jni_layer.cpp | 11 +++++-- extension/android/jni/jni_layer_llama.cpp | 6 ++++ 6 files changed, 36 insertions(+), 20 deletions(-) diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java index 30ebf1a2c1d..6ea2a12dc64 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java @@ -41,7 +41,8 @@ public static ExecuTorchRuntime getRuntime() { public static void validateFilePath(String path, String description) { File file = new File(path); if (!file.canRead() || !file.isFile()) { - throw new RuntimeException("Cannot load " + description + " " + path); + throw new IllegalArgumentException( + "Cannot load " + description + ": " + path + " does not exist or is not readable"); } } diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt index 987cb3ec3be..8d8b7f3a2bc 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt @@ -160,7 +160,7 @@ class AsrModule( ) if (status != 0) { - throw RuntimeException("Transcription failed with error code: $status") + throw org.pytorch.executorch.ExecutorchRuntimeException(status, "Transcription failed") } return result.toString() diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.java b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.java index 8f4292c1bc8..58c7704b83e 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.java +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.java @@ -93,7 +93,7 @@ public static SGD create(Map namedParameters, double learningRat */ public void step(Map namedGradients) { if (!mHybridData.isValid()) { - throw new RuntimeException("Attempt to use a destroyed SGD optimizer"); + throw new IllegalStateException("SGD optimizer has been destroyed"); } stepNative(namedGradients); } diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.java b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.java index 4a6653cb7a1..ca4bac9aa54 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.java +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.java @@ -8,12 +8,11 @@ package org.pytorch.executorch.training; -import android.util.Log; import com.facebook.jni.HybridData; import com.facebook.jni.annotations.DoNotStrip; import com.facebook.soloader.nativeloader.NativeLoader; import com.facebook.soloader.nativeloader.SystemDelegate; -import java.util.HashMap; +import java.io.Closeable; import java.util.Map; import org.pytorch.executorch.EValue; import org.pytorch.executorch.ExecuTorchRuntime; @@ -26,7 +25,7 @@ *

Warning: These APIs are experimental and subject to change without notice */ @Experimental -public class TrainingModule { +public class TrainingModule implements Closeable { static { if (!NativeLoader.isInitialized()) { @@ -37,6 +36,7 @@ public class TrainingModule { } private final HybridData mHybridData; + private boolean mDestroyed = false; @DoNotStrip private static native HybridData initHybrid(String moduleAbsolutePath, String dataAbsolutePath); @@ -45,6 +45,10 @@ private TrainingModule(String moduleAbsolutePath, String dataAbsolutePath) { mHybridData = initHybrid(moduleAbsolutePath, dataAbsolutePath); } + private void checkNotDestroyed() { + if (mDestroyed) throw new IllegalStateException("TrainingModule has been destroyed"); + } + /** * Loads a serialized ExecuTorch Training Module from the specified path on the disk. * @@ -78,10 +82,7 @@ public static TrainingModule load(final String modelPath) { * @return return value(s) from the method. */ public EValue[] executeForwardBackward(String methodName, EValue... inputs) { - if (!mHybridData.isValid()) { - Log.e("ExecuTorch", "Attempt to use a destroyed module"); - return new EValue[0]; - } + checkNotDestroyed(); return executeForwardBackwardNative(methodName, inputs); } @@ -89,10 +90,7 @@ public EValue[] executeForwardBackward(String methodName, EValue... inputs) { private native EValue[] executeForwardBackwardNative(String methodName, EValue... inputs); public Map namedParameters(String methodName) { - if (!mHybridData.isValid()) { - Log.e("ExecuTorch", "Attempt to use a destroyed module"); - return new HashMap(); - } + checkNotDestroyed(); return namedParametersNative(methodName); } @@ -100,13 +98,17 @@ public Map namedParameters(String methodName) { private native Map namedParametersNative(String methodName); public Map namedGradients(String methodName) { - if (!mHybridData.isValid()) { - Log.e("ExecuTorch", "Attempt to use a destroyed module"); - return new HashMap(); - } + checkNotDestroyed(); return namedGradientsNative(methodName); } @DoNotStrip private native Map namedGradientsNative(String methodName); + + @Override + public void close() { + if (mDestroyed) return; + mDestroyed = true; + mHybridData.resetNative(); + } } diff --git a/extension/android/jni/jni_layer.cpp b/extension/android/jni/jni_layer.cpp index 88e9f9e2a12..a1eecd05b9c 100644 --- a/extension/android/jni/jni_layer.cpp +++ b/extension/android/jni/jni_layer.cpp @@ -284,8 +284,15 @@ class ExecuTorchJni : public facebook::jni::HybridClass { #else auto etdump_gen = nullptr; #endif - module_ = std::make_unique( - modelPath->toStdString(), load_mode, std::move(etdump_gen)); + try { + module_ = std::make_unique( + modelPath->toStdString(), load_mode, std::move(etdump_gen)); + } catch (const std::exception& e) { + executorch::jni_helper::throwExecutorchException( + static_cast(Error::Internal), + std::string("Failed to create Module: ") + e.what()); + return; + } #ifdef ET_USE_THREADPOOL // Default to using cores/2 threadpool threads. The long-term plan is to diff --git a/extension/android/jni/jni_layer_llama.cpp b/extension/android/jni/jni_layer_llama.cpp index 2c0117dc576..f903a63d202 100644 --- a/extension/android/jni/jni_layer_llama.cpp +++ b/extension/android/jni/jni_layer_llama.cpp @@ -148,6 +148,7 @@ class ExecuTorchLlmJni : public facebook::jni::HybridClass { jint num_bos = 0, jint num_eos = 0, jint load_mode = 1) { + try { temperature_ = temperature; num_bos_ = num_bos; num_eos_ = num_eos; @@ -246,6 +247,11 @@ class ExecuTorchLlmJni : public facebook::jni::HybridClass { model_type_category_ = MODEL_TYPE_CATEGORY_LLM; #endif } + } catch (const std::exception& e) { + executorch::jni_helper::throwExecutorchException( + static_cast(Error::Internal), + std::string("Failed to create LlmModule: ") + e.what()); + } } jint generate( From 073360ff7dd38e35b23d349cc074005479a68178 Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Thu, 23 Apr 2026 15:38:42 -0700 Subject: [PATCH 2/8] Fix last bare RuntimeException in AsrModule.kt Native creation failure now throws ExecutorchRuntimeException with INTERNAL error code, consistent with all other modules. This commit was authored with the help of Claude. --- .../java/org/pytorch/executorch/extension/asr/AsrModule.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt index 8d8b7f3a2bc..a13299d8920 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt @@ -53,7 +53,9 @@ class AsrModule( val handle = nativeCreate(modelPath, tokenizerPath, dataPath, preprocessorPath) if (handle == 0L) { - throw RuntimeException("Failed to create native AsrModule") + throw org.pytorch.executorch.ExecutorchRuntimeException( + org.pytorch.executorch.ExecutorchRuntimeException.INTERNAL, + "Failed to create native AsrModule") } nativeHandle.set(handle) } From d40574d49d62efbc9d7e6c104d1aefbd20f94fc4 Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Thu, 23 Apr 2026 15:51:22 -0700 Subject: [PATCH 3/8] Address review: lint, rethrow in JNI constructors, Javadoc, imports - Fix clang-format indentation in jni_layer_llama.cpp constructor - Add throw; after throwExecutorchException in both JNI constructors to ensure construction fails deterministically - Use import for ExecutorchRuntimeException in AsrModule.kt - Fix validateFilePath Javadoc to document IllegalArgumentException - Split validateFilePath into specific error messages (does not exist, is not a file, is not readable) This commit was authored with the help of Claude. --- .../pytorch/executorch/ExecuTorchRuntime.java | 14 +- .../executorch/extension/asr/AsrModule.kt | 5 +- extension/android/jni/jni_layer.cpp | 2 +- extension/android/jni/jni_layer_llama.cpp | 183 +++++++++--------- 4 files changed, 109 insertions(+), 95 deletions(-) diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java index 6ea2a12dc64..7035acb3527 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java @@ -36,13 +36,21 @@ public static ExecuTorchRuntime getRuntime() { /** * Validates that the given path points to a readable file. * - * @throws RuntimeException if the file does not exist or is not readable. + * @throws IllegalArgumentException if the path does not exist, is not a file, or is not readable. */ public static void validateFilePath(String path, String description) { File file = new File(path); - if (!file.canRead() || !file.isFile()) { + if (!file.exists()) { throw new IllegalArgumentException( - "Cannot load " + description + ": " + path + " does not exist or is not readable"); + "Cannot load " + description + ": " + path + " does not exist"); + } + if (!file.isFile()) { + throw new IllegalArgumentException( + "Cannot load " + description + ": " + path + " is not a file"); + } + if (!file.canRead()) { + throw new IllegalArgumentException( + "Cannot load " + description + ": " + path + " is not readable"); } } diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt index a13299d8920..8d8e4b46c5a 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt @@ -11,6 +11,7 @@ package org.pytorch.executorch.extension.asr import java.io.Closeable import java.io.File import java.util.concurrent.atomic.AtomicLong +import org.pytorch.executorch.ExecutorchRuntimeException import org.pytorch.executorch.annotations.Experimental /** @@ -53,7 +54,7 @@ class AsrModule( val handle = nativeCreate(modelPath, tokenizerPath, dataPath, preprocessorPath) if (handle == 0L) { - throw org.pytorch.executorch.ExecutorchRuntimeException( + throw ExecutorchRuntimeException( org.pytorch.executorch.ExecutorchRuntimeException.INTERNAL, "Failed to create native AsrModule") } @@ -162,7 +163,7 @@ class AsrModule( ) if (status != 0) { - throw org.pytorch.executorch.ExecutorchRuntimeException(status, "Transcription failed") + throw ExecutorchRuntimeException(status, "Transcription failed") } return result.toString() diff --git a/extension/android/jni/jni_layer.cpp b/extension/android/jni/jni_layer.cpp index a1eecd05b9c..352b563339a 100644 --- a/extension/android/jni/jni_layer.cpp +++ b/extension/android/jni/jni_layer.cpp @@ -291,7 +291,7 @@ class ExecuTorchJni : public facebook::jni::HybridClass { executorch::jni_helper::throwExecutorchException( static_cast(Error::Internal), std::string("Failed to create Module: ") + e.what()); - return; + throw; } #ifdef ET_USE_THREADPOOL diff --git a/extension/android/jni/jni_layer_llama.cpp b/extension/android/jni/jni_layer_llama.cpp index f903a63d202..e71ab57adf5 100644 --- a/extension/android/jni/jni_layer_llama.cpp +++ b/extension/android/jni/jni_layer_llama.cpp @@ -149,108 +149,113 @@ class ExecuTorchLlmJni : public facebook::jni::HybridClass { jint num_eos = 0, jint load_mode = 1) { try { - temperature_ = temperature; - num_bos_ = num_bos; - num_eos_ = num_eos; + temperature_ = temperature; + num_bos_ = num_bos; + num_eos_ = num_eos; #if defined(ET_USE_THREADPOOL) - // Reserve 1 thread for the main thread. - int32_t num_performant_cores = - ::executorch::extension::cpuinfo::get_num_performant_cores() - 1; - if (num_performant_cores > 0) { - ET_LOG(Info, "Resetting threadpool to %d threads", num_performant_cores); - ::executorch::extension::threadpool::get_threadpool() - ->_unsafe_reset_threadpool(num_performant_cores); - } + // Reserve 1 thread for the main thread. + int32_t num_performant_cores = + ::executorch::extension::cpuinfo::get_num_performant_cores() - 1; + if (num_performant_cores > 0) { + ET_LOG( + Info, "Resetting threadpool to %d threads", num_performant_cores); + ::executorch::extension::threadpool::get_threadpool() + ->_unsafe_reset_threadpool(num_performant_cores); + } #endif - model_type_category_ = model_type_category; - auto cpp_load_mode = load_mode_from_int(load_mode); - std::vector data_files_vector; - if (model_type_category == MODEL_TYPE_CATEGORY_MULTIMODAL) { - runner_ = llm::create_multimodal_runner( - model_path->toStdString().c_str(), - llm::load_tokenizer(tokenizer_path->toStdString()), - std::nullopt, - cpp_load_mode); - } else if (model_type_category == MODEL_TYPE_CATEGORY_LLM) { - if (data_files != nullptr) { - // Convert Java List to C++ std::vector - auto list_class = facebook::jni::findClassStatic("java/util/List"); - auto size_method = list_class->getMethod("size"); - auto get_method = - list_class->getMethod(jint)>( - "get"); - - jint size = size_method(data_files); - for (jint i = 0; i < size; ++i) { - auto str_obj = get_method(data_files, i); - auto jstr = facebook::jni::static_ref_cast(str_obj); - data_files_vector.push_back(jstr->toStdString()); + model_type_category_ = model_type_category; + auto cpp_load_mode = load_mode_from_int(load_mode); + std::vector data_files_vector; + if (model_type_category == MODEL_TYPE_CATEGORY_MULTIMODAL) { + runner_ = llm::create_multimodal_runner( + model_path->toStdString().c_str(), + llm::load_tokenizer(tokenizer_path->toStdString()), + std::nullopt, + cpp_load_mode); + } else if (model_type_category == MODEL_TYPE_CATEGORY_LLM) { + if (data_files != nullptr) { + // Convert Java List to C++ std::vector + auto list_class = facebook::jni::findClassStatic("java/util/List"); + auto size_method = list_class->getMethod("size"); + auto get_method = + list_class->getMethod(jint)>( + "get"); + + jint size = size_method(data_files); + for (jint i = 0; i < size; ++i) { + auto str_obj = get_method(data_files, i); + auto jstr = facebook::jni::static_ref_cast(str_obj); + data_files_vector.push_back(jstr->toStdString()); + } } - } - runner_ = executorch::extension::llm::create_text_llm_runner( - model_path->toStdString(), - llm::load_tokenizer(tokenizer_path->toStdString()), - data_files_vector, - /*temperature=*/-1.0f, - /*event_tracer=*/nullptr, - /*method_name=*/"forward", - cpp_load_mode); + runner_ = executorch::extension::llm::create_text_llm_runner( + model_path->toStdString(), + llm::load_tokenizer(tokenizer_path->toStdString()), + data_files_vector, + /*temperature=*/-1.0f, + /*event_tracer=*/nullptr, + /*method_name=*/"forward", + cpp_load_mode); #if defined(EXECUTORCH_BUILD_QNN) - } else if (model_type_category == MODEL_TYPE_QNN_LLAMA) { - std::unique_ptr module = - std::make_unique( - model_path->toStdString().c_str(), - data_files_vector, - cpp_load_mode); - std::string decoder_model = "llama3"; // use llama3 for now - // Using 8bit as default since this meta is introduced with 16bit kv io - // support and older models only have 8bit kv io. - example::KvBitWidth kv_bitwidth = example::KvBitWidth::kWidth8; - if (module->method_names()->count("get_kv_io_bit_width") > 0) { - kv_bitwidth = static_cast( - module->get("get_kv_io_bit_width").get().toScalar().to()); - } + } else if (model_type_category == MODEL_TYPE_QNN_LLAMA) { + std::unique_ptr module = + std::make_unique( + model_path->toStdString().c_str(), + data_files_vector, + cpp_load_mode); + std::string decoder_model = "llama3"; // use llama3 for now + // Using 8bit as default since this meta is introduced with 16bit kv io + // support and older models only have 8bit kv io. + example::KvBitWidth kv_bitwidth = example::KvBitWidth::kWidth8; + if (module->method_names()->count("get_kv_io_bit_width") > 0) { + kv_bitwidth = static_cast( + module->get("get_kv_io_bit_width") + .get() + .toScalar() + .to()); + } - if (kv_bitwidth == example::KvBitWidth::kWidth8) { - runner_ = std::make_unique>( - std::move(module), - decoder_model.c_str(), - model_path->toStdString().c_str(), - tokenizer_path->toStdString().c_str(), - "", - "", - temperature_); - } else if (kv_bitwidth == example::KvBitWidth::kWidth16) { - runner_ = std::make_unique>( - std::move(module), - decoder_model.c_str(), - model_path->toStdString().c_str(), - tokenizer_path->toStdString().c_str(), - "", - "", - temperature_); - } else { - ET_CHECK_MSG( - false, - "Unsupported kv bitwidth: %ld", - static_cast(kv_bitwidth)); - } - model_type_category_ = MODEL_TYPE_CATEGORY_LLM; + if (kv_bitwidth == example::KvBitWidth::kWidth8) { + runner_ = std::make_unique>( + std::move(module), + decoder_model.c_str(), + model_path->toStdString().c_str(), + tokenizer_path->toStdString().c_str(), + "", + "", + temperature_); + } else if (kv_bitwidth == example::KvBitWidth::kWidth16) { + runner_ = std::make_unique>( + std::move(module), + decoder_model.c_str(), + model_path->toStdString().c_str(), + tokenizer_path->toStdString().c_str(), + "", + "", + temperature_); + } else { + ET_CHECK_MSG( + false, + "Unsupported kv bitwidth: %ld", + static_cast(kv_bitwidth)); + } + model_type_category_ = MODEL_TYPE_CATEGORY_LLM; #endif #if defined(EXECUTORCH_BUILD_MEDIATEK) - } else if (model_type_category == MODEL_TYPE_MEDIATEK_LLAMA) { - runner_ = std::make_unique( - model_path->toStdString().c_str(), - tokenizer_path->toStdString().c_str()); - // Interpret the model type as LLM - model_type_category_ = MODEL_TYPE_CATEGORY_LLM; + } else if (model_type_category == MODEL_TYPE_MEDIATEK_LLAMA) { + runner_ = std::make_unique( + model_path->toStdString().c_str(), + tokenizer_path->toStdString().c_str()); + // Interpret the model type as LLM + model_type_category_ = MODEL_TYPE_CATEGORY_LLM; #endif - } + } } catch (const std::exception& e) { executorch::jni_helper::throwExecutorchException( static_cast(Error::Internal), std::string("Failed to create LlmModule: ") + e.what()); + throw; } } From 1dd4eccba02ca242c4c1b1ca4234b60bd6df96df Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Thu, 23 Apr 2026 20:29:38 -0700 Subject: [PATCH 4/8] Add catch(...) for non-std exceptions, fix qualified import JNI constructors now catch all native exceptions, not just std::exception. Fix redundant fully-qualified ExecutorchRuntimeException.INTERNAL in AsrModule.kt. This commit was authored with the help of Claude. --- .../java/org/pytorch/executorch/extension/asr/AsrModule.kt | 2 +- extension/android/jni/jni_layer.cpp | 5 +++++ extension/android/jni/jni_layer_llama.cpp | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt index 8d8e4b46c5a..063f0c3112b 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt @@ -55,7 +55,7 @@ class AsrModule( val handle = nativeCreate(modelPath, tokenizerPath, dataPath, preprocessorPath) if (handle == 0L) { throw ExecutorchRuntimeException( - org.pytorch.executorch.ExecutorchRuntimeException.INTERNAL, + ExecutorchRuntimeException.INTERNAL, "Failed to create native AsrModule") } nativeHandle.set(handle) diff --git a/extension/android/jni/jni_layer.cpp b/extension/android/jni/jni_layer.cpp index 352b563339a..5942e810a9a 100644 --- a/extension/android/jni/jni_layer.cpp +++ b/extension/android/jni/jni_layer.cpp @@ -292,6 +292,11 @@ class ExecuTorchJni : public facebook::jni::HybridClass { static_cast(Error::Internal), std::string("Failed to create Module: ") + e.what()); throw; + } catch (...) { + executorch::jni_helper::throwExecutorchException( + static_cast(Error::Internal), + "Failed to create Module: unknown native error"); + throw; } #ifdef ET_USE_THREADPOOL diff --git a/extension/android/jni/jni_layer_llama.cpp b/extension/android/jni/jni_layer_llama.cpp index 29d30c30e5f..d59a49da167 100644 --- a/extension/android/jni/jni_layer_llama.cpp +++ b/extension/android/jni/jni_layer_llama.cpp @@ -257,6 +257,11 @@ class ExecuTorchLlmJni : public facebook::jni::HybridClass { static_cast(Error::Internal), std::string("Failed to create LlmModule: ") + e.what()); throw; + } catch (...) { + executorch::jni_helper::throwExecutorchException( + static_cast(Error::Internal), + "Failed to create LlmModule: unknown native error"); + throw; } } From 965b07591d984228fdcc66337ec073837f693614 Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Thu, 23 Apr 2026 23:11:07 -0700 Subject: [PATCH 5/8] Fix spotlessKotlinCheck: add trailing comma in AsrModule.kt This commit was authored with the help of Claude. --- .../java/org/pytorch/executorch/extension/asr/AsrModule.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt index 063f0c3112b..a1ca0d0dd0d 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt @@ -56,7 +56,8 @@ class AsrModule( if (handle == 0L) { throw ExecutorchRuntimeException( ExecutorchRuntimeException.INTERNAL, - "Failed to create native AsrModule") + "Failed to create native AsrModule", + ) } nativeHandle.set(handle) } From 1ef1d1d6b808499361555be651245d8348f8cd11 Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Thu, 23 Apr 2026 23:30:20 -0700 Subject: [PATCH 6/8] Add null check in validateFilePath, fix AsrModule @throws doc This commit was authored with the help of Claude. --- .../main/java/org/pytorch/executorch/ExecuTorchRuntime.java | 6 +++++- .../java/org/pytorch/executorch/extension/asr/AsrModule.kt | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java index 7035acb3527..ac4311eb1f7 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java @@ -36,9 +36,13 @@ public static ExecuTorchRuntime getRuntime() { /** * Validates that the given path points to a readable file. * - * @throws IllegalArgumentException if the path does not exist, is not a file, or is not readable. + * @throws IllegalArgumentException if the path is null, does not exist, is not a file, or is not + * readable. */ public static void validateFilePath(String path, String description) { + if (path == null) { + throw new IllegalArgumentException("Cannot load " + description + ": path is null"); + } File file = new File(path); if (!file.exists()) { throw new IllegalArgumentException( diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt index a1ca0d0dd0d..ab9099ba405 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.kt @@ -133,7 +133,7 @@ class AsrModule( * @param callback Optional callback to receive tokens as they are generated (can be null) * @return The complete transcribed text * @throws IllegalStateException if the module has been destroyed - * @throws RuntimeException if transcription fails (non-zero result code) + * @throws ExecutorchRuntimeException if transcription fails (error code carried in exception) */ @JvmOverloads fun transcribe( From 27ec438b91517f5a90cce9d0335e7a47efc3804c Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Fri, 24 Apr 2026 15:12:23 -0700 Subject: [PATCH 7/8] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../org/pytorch/executorch/ExecuTorchRuntime.java | 9 +++------ extension/android/jni/jni_layer.cpp | 14 ++++++++------ extension/android/jni/jni_layer_llama.cpp | 2 -- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java index ac4311eb1f7..53ee4d3f33a 100644 --- a/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java +++ b/extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.java @@ -45,16 +45,13 @@ public static void validateFilePath(String path, String description) { } File file = new File(path); if (!file.exists()) { - throw new IllegalArgumentException( - "Cannot load " + description + ": " + path + " does not exist"); + throw new IllegalArgumentException("Cannot load " + description + "!! " + path); } if (!file.isFile()) { - throw new IllegalArgumentException( - "Cannot load " + description + ": " + path + " is not a file"); + throw new IllegalArgumentException("Cannot load " + description + "!! " + path); } if (!file.canRead()) { - throw new IllegalArgumentException( - "Cannot load " + description + ": " + path + " is not readable"); + throw new IllegalArgumentException("Cannot load " + description + "!! " + path); } } diff --git a/extension/android/jni/jni_layer.cpp b/extension/android/jni/jni_layer.cpp index 5942e810a9a..1ed3aac7afc 100644 --- a/extension/android/jni/jni_layer.cpp +++ b/extension/android/jni/jni_layer.cpp @@ -288,15 +288,17 @@ class ExecuTorchJni : public facebook::jni::HybridClass { module_ = std::make_unique( modelPath->toStdString(), load_mode, std::move(etdump_gen)); } catch (const std::exception& e) { + const std::string error_message = + std::string("Failed to create Module: ") + e.what(); executorch::jni_helper::throwExecutorchException( - static_cast(Error::Internal), - std::string("Failed to create Module: ") + e.what()); - throw; + static_cast(Error::Internal), error_message); + throw facebook::jni::JniException(error_message); } catch (...) { + constexpr const char* error_message = + "Failed to create Module: unknown native error"; executorch::jni_helper::throwExecutorchException( - static_cast(Error::Internal), - "Failed to create Module: unknown native error"); - throw; + static_cast(Error::Internal), error_message); + throw facebook::jni::JniException(error_message); } #ifdef ET_USE_THREADPOOL diff --git a/extension/android/jni/jni_layer_llama.cpp b/extension/android/jni/jni_layer_llama.cpp index d59a49da167..0c1ff5c67b9 100644 --- a/extension/android/jni/jni_layer_llama.cpp +++ b/extension/android/jni/jni_layer_llama.cpp @@ -256,12 +256,10 @@ class ExecuTorchLlmJni : public facebook::jni::HybridClass { executorch::jni_helper::throwExecutorchException( static_cast(Error::Internal), std::string("Failed to create LlmModule: ") + e.what()); - throw; } catch (...) { executorch::jni_helper::throwExecutorchException( static_cast(Error::Internal), "Failed to create LlmModule: unknown native error"); - throw; } } From 1d00327c4f9c44c1c60fcb3406d000381504e705 Mon Sep 17 00:00:00 2001 From: Siddartha Pothapragada Date: Fri, 24 Apr 2026 15:29:58 -0700 Subject: [PATCH 8/8] Fix build: remove invalid JniException constructor call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JniException takes a jthrowable, not a string. throwExecutorchException already throws via fbjni at the JNI boundary — no need to rethrow. This commit was authored with the help of Claude. --- extension/android/jni/jni_layer.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/extension/android/jni/jni_layer.cpp b/extension/android/jni/jni_layer.cpp index 1ed3aac7afc..0cf08e41983 100644 --- a/extension/android/jni/jni_layer.cpp +++ b/extension/android/jni/jni_layer.cpp @@ -288,17 +288,13 @@ class ExecuTorchJni : public facebook::jni::HybridClass { module_ = std::make_unique( modelPath->toStdString(), load_mode, std::move(etdump_gen)); } catch (const std::exception& e) { - const std::string error_message = - std::string("Failed to create Module: ") + e.what(); executorch::jni_helper::throwExecutorchException( - static_cast(Error::Internal), error_message); - throw facebook::jni::JniException(error_message); + static_cast(Error::Internal), + std::string("Failed to create Module: ") + e.what()); } catch (...) { - constexpr const char* error_message = - "Failed to create Module: unknown native error"; executorch::jni_helper::throwExecutorchException( - static_cast(Error::Internal), error_message); - throw facebook::jni::JniException(error_message); + static_cast(Error::Internal), + "Failed to create Module: unknown native error"); } #ifdef ET_USE_THREADPOOL