Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229) - #19229

Merged
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2
May 14, 2026
Merged

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)#19229
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2

Conversation

@psiddh

@psiddhpsiddh commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Summary:
Combines all previously reverted Android improvement PRs (#18669, #19012, #19028, #19092, #19099, #19124) plus new Module lifecycle tests into a single atomic change.

Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.

Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.

Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.

JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).

Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.

This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.

Reviewed By: kirklandsign

CopilotAI review requested due to automatic review settings April 30, 2026 16:38
@pytorch-bot

pytorch-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/19229

Note: Links to docs will display an error until the docs builds have been completed.

❗ 1 Active SEVs

There are 1 currently active SEVs. If your PR is affected, please view them below:

❌ 2 New Failures, 4 Unrelated Failures

As of commit 85cc5e4 with merge base fe98297 (image):

NEW FAILURES - The following jobs have failed:

FLAKY - The following job failed but was likely due to flakiness present on trunk:

BROKEN TRUNK - The following jobs failed but was present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Apr 30, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Consolidates several Android API improvements across the ExecuTorch Java/Kotlin wrappers and JNI layer, focusing on exception-based error reporting, explicit lifecycle management (Closeable), and basic thread-safety guarantees, plus expanded instrumentation test coverage.

Changes:

  • Standardize synchronous error handling to throw exceptions (vs returning status codes / silent logging) across Module, LlmModule, training/ASR wrappers, and JNI glue.
  • Add/solidify lifecycle management via Closeable (close()/idempotent destroy patterns) and serialize access to non-thread-safe native state in LlmModule via ReentrantLock.
  • Update JNI registrations and Java native method naming (*Native), and expand Android instrumentation tests to cover lifecycle and API behavior.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaHandle Module.loadMethod() throwing by capturing an error code and early-returning safely; ensure destroy() in finally.
extension/android/jni/jni_layer_llama.cppAdd constructor exception-to-Java conversion; improve load() error reporting; rename registered natives (generateNative, loadNative, resetContextNative).
extension/android/jni/jni_layer.cppWrap native Module construction in exception mapping; throw on unsupported input EValue type codes; rename registered natives (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplement Closeable; replace silent failures with IllegalStateException on use-after-destroy.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaThrow IllegalStateException (vs generic RuntimeException) when optimizer is destroyed.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplement Closeable; add locking + destroyed checks; convert many status-returning APIs to void + exception; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktThrow ExecutorchRuntimeException with error code on creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplement Closeable; make loadMethod() throw; add locked wrappers around additional APIs; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdd error-code documentation, ALREADY_LOADED, improved message prefix, and cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaTighten file-path validation and switch to IllegalArgumentException.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignore and fix tests by providing required dummy input; add new API/lifecycle coverage.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdate tests for exception-based load; add close() lifecycle tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a96bd6b to 6ffdd62CompareApril 30, 2026 16:47
CopilotAI review requested due to automatic review settings April 30, 2026 17:05
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 787239e to 9f8afe1CompareApril 30, 2026 17:06
@psiddh
psiddh marked this pull request as draft April 30, 2026 17:10

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR consolidates several previously reverted Android improvements into one atomic update, standardizing ExecuTorch Android APIs around exception-based error reporting, explicit Closeable lifecycles, and improved thread-safety—along with significant new/updated instrumentation test coverage.

Changes:

  • Shift Android APIs away from status-code returns/silent failures to consistent exception throwing (Java/Kotlin + JNI).
  • Add/align Closeable lifecycle semantics (try-with-resources) and introduce locking around non-thread-safe native state (notably for LlmModule).
  • Update JNI registrations to match renamed *Native methods and expand Android instrumentation tests (including new lifecycle tests).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaAdapts benchmark load-path to exception-based Module.loadMethod() and ensures destroy on success path.
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.javaAdapts load-path to exception-based LlmModule.load() (but generate path still needs updates).
extension/android/jni/jni_layer_llama.cppAdds constructor error surfacing, improves load error reporting, and updates native method registrations (*Native).
extension/android/jni/jni_layer.cppAdds constructor error surfacing, throws on unsupported EValue input type, and updates native registrations (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplements Closeable and replaces silent failures with exceptions.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaUses IllegalStateException for destroyed optimizer usage.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplements Closeable, adds locking, converts sync APIs to throw-on-error wrappers, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktSwitches to ExecutorchRuntimeException for native creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplements Closeable, adds throw-on-error wrappers and lock-guarded public methods, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdds richer docs, new error code constant, improved message format, and a cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaMakes file-path validation stricter and consistently IllegalArgumentException-based.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignores/fixes Module tests and adds broader API/lifecycle coverage (load modes, log buffer, etdump, destroyed-state).
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdates to new throwing APIs and adds lifecycle tests (use-after-close, idempotent close).
Comments suppressed due to low confidence (1)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:102

  • LlmModule.generate(...) now throws on failure, but the MESSAGE_GENERATE branch doesn't catch exceptions. An exception here will crash the HandlerThread and can prevent onGenerationStopped() from running / results from being recorded. Wrap the generate call in try/catch (ideally extracting an error code from ExecutorchRuntimeException) and route the failure to the callback (e.g., call onGenerationStopped() and/or add an explicit error callback).
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has imported this pull request. If you are a Meta employee, you can view this in D103233465.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a44e428 to 04727f0CompareApril 30, 2026 18:19
@meta-codesyncmeta-codesyncBot changed the title Android: Closeable lifecycle, error reporting, thread safety, and tes…Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)May 5, 2026
psiddh added a commit to psiddh/executorch that referenced this pull request May 5, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 9adb92f to 274552bCompareMay 5, 2026 16:13
@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has exported this pull request. If you are a Meta employee, you can view the originating Diff in D103233465.

@psiddh
psiddh marked this pull request as ready for review May 5, 2026 16:18
CopilotAI review requested due to automatic review settings May 5, 2026 16:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:103

  • LlmModule.generate(...) now throws on failure (it no longer returns a status code). In the MESSAGE_GENERATE path, any exception will escape handleMessage, potentially killing the handler thread and skipping onGenerationStopped(). Wrap generate() in try/catch and ensure the callback is notified (and/or propagate an error status) instead of letting the background thread crash.
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.kt:62

  • These tests create a new LlmModule in setUp() for every test case but never close it, so native resources can accumulate across the suite (and potentially cause flakiness/OOM). Add an @After that calls llmModule.close() (or wrap each test body in try/finally) so the module is always released.
 llmModule =
LlmModule(getTestFilePath(TEST_FILE_NAME), getTestFilePath(TOKENIZER_FILE_NAME), 0.0f)
}
@Test
@Throws(IOException::class, URISyntaxException::class)
fun testGenerate() {
llmModule.load()
llmModule.generate(TEST_PROMPT, SEQ_LEN, this@LlmModuleInstrumentationTest)
assertEquals(results.size.toLong(), SEQ_LEN.toLong())
assertTrue(tokensPerSecond[tokensPerSecond.size - 1] > 0)
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/android/jni/jni_layer.cpp
@psiddh

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR and the comments

@claude

claudeBot commented May 8, 2026

Copy link
Copy Markdown

Claude finished @psiddh's task in 2m 14s —— View job


PR Review: Android Closeable lifecycle, error reporting, thread safety, and test coverage

  • Gather context and understand the request
  • Review core Java/Kotlin changes (Module, LlmModule, error handling)
  • Review JNI layer changes
  • Review benchmark/caller updates
  • Review test coverage
  • Assess existing review comments and provide consolidated feedback

Overall Assessment

This is a well-structured consolidation of several improvements. The move from status-code returns to exception-based error reporting is the right direction for Android APIs, and the Closeable lifecycle + ReentrantLock serialization are meaningful safety improvements. The test coverage additions are solid.

That said, there are a few issues worth addressing — some flagged by Copilot that I agree with, and some additional ones.


Issues I'd Recommend Fixing

1. stop() has no use-after-close guard (High Priority)

File:LlmModule.java:782

stop() is a bare public native void stop() with no mDestroyed check. Every other public method checks mDestroyed and throws IllegalStateException, but stop() would call into released native memory after close(). Since stop() intentionally skips the lock (it uses a C++ atomic flag), a simple volatile check is sufficient:

publicvoidstop() {
if (mDestroyed) thrownewIllegalStateException("LlmModule has been destroyed");
stopNative();
}
@DoNotStripprivatenativevoidstopNative();

This also requires updating the JNI registration in jni_layer_llama.cpp to map "stopNative"ExecuTorchLlmJni::stop.

Fix this →

2. Missing std::move for etdump_gen in profiling build (Medium Priority)

File:jni_layer.cpp:288-289

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, etdump_gen);

The Module constructor takes std::unique_ptr<runtime::EventTracer> by value. In the EXECUTORCH_ANDROID_PROFILING path, etdump_gen is a std::unique_ptr<ETDumpGen> — passing it without std::move won't compile. This should be:

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, std::move(etdump_gen));

The non-profiling path (auto etdump_gen = nullptr) works fine since nullptr implicitly converts to a null unique_ptr.

Fix this →

3. LlmModelRunner generate path lacks exception handling (Medium Priority)

File:LlmModelRunner.java:101-103

} elseif (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

generate() now throws ExecutorchRuntimeException on failure, but this path doesn't catch it. An exception here kills the HandlerThread and skips onGenerationStopped(). Should be wrapped like the load path:

} elseif (msg.what == MESSAGE_GENERATE) {
try {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
} catch (Exceptione) {
// Optionally surface error to callback
}
mLlmModelRunner.mCallback.onGenerationStopped();
}

Fix this →

4. LlmModuleInstrumentationTest leaks native resources (Low Priority)

File:LlmModuleInstrumentationTest.kt

setUp() creates a new LlmModule for each test but there's no @After method calling close(). Tests that don't explicitly close (e.g., testGenerate, testGenerateAndStop, all the prefill validation tests) leak native memory. Add:

@After
funtearDown() {
if (::llmModule.isInitialized) {
llmModule.close()
}
}

Minor Observations (Non-blocking)

5. testMethodMetadata() is a no-op test

File:ModuleInstrumentationTest.kt:56-59

funtestMethodMetadata() {
val module =Module.load(getTestFilePath(TEST_FILE_NAME))
module.destroy()
}

This test loads and immediately destroys without asserting anything about metadata. The newer testGetMethodMetadata() at line 216 does the actual work. The old test is dead code — consider removing it.

6. testNonPteFile doesn't clean up on unexpected success

File:ModuleInstrumentationTest.kt:112-117

funtestNonPteFile() {
val module =Module.load(getTestFilePath(NON_PTE_FILE_NAME))
module.loadMethod(FORWARD_METHOD) // expected to throw
}

If loadMethod doesn't throw (test expectation fails), the module is never destroyed. Consider using assertThrows with a finally block for cleanup.

7. ReentrantLock callback re-entrancy in LlmModule

File:LlmModule.java:322-331

The generate() method holds the ReentrantLock while native code invokes LlmCallback methods. Since the lock is reentrant, callbacks running on the same thread could re-enter other LlmModule methods (e.g., resetContext(), close()). The close() has a getHoldCount() > 1 guard, but other methods don't. This is an inherent limitation of using ReentrantLock — documenting that callbacks must not call back into LlmModule (except stop()) would help.


What Looks Good

  • ExecutorchRuntimeException: The error code mapping with human-readable messages and hex codes is excellent for debugging. The makeExecutorchException factory pattern is clean.
  • ExecuTorchRuntime.validateFilePath: Clear, specific error messages for each failure case (null, doesn't exist, not a file, not readable). This addresses the earlier Copilot feedback about !! — the current implementation is already clean.
  • Module.close()/destroy() pattern: Using tryLock() to detect concurrent execution and throwing rather than blocking is the right call for lifecycle methods.
  • TrainingModule: Simple and correct — checkNotDestroyed() + Closeable with idempotent close().
  • AsrModule: Good use of AtomicLong + getAndSet(0L) for thread-safe idempotent close without needing a lock.
  • JNI error surfacing: Both jni_layer.cpp and jni_layer_llama.cpp now catch native exceptions during construction and convert them to Java exceptions — this was a significant gap before.
  • Test coverage: The multi-threaded forward test, destroyed-state tests for all public methods, and the LLM lifecycle tests provide good confidence.

Summary

The PR is in good shape overall. The 3 higher-priority items (#1 stop() guard, #2 std::move, #3 generate exception handling) are worth fixing before merge as they represent potential crashes or UB. The rest are polish items.


CopilotAI review requested due to automatic review settings May 12, 2026 21:39
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 4558d37 to 0415dd1CompareMay 12, 2026 21:46

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment threadextension/android/jni/jni_layer.cpp
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 0415dd1 to e72eecdCompareMay 12, 2026 21:55
psiddh added a commit to psiddh/executorch that referenced this pull request May 13, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
CopilotAI review requested due to automatic review settings May 13, 2026 06:04
@psiddh
psiddhforce-pushed the android-combined-v2 branch from e72eecd to 881a8d5CompareMay 13, 2026 06:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 881a8d5 to 85cc5e4CompareMay 13, 2026 07:56
@meta-codesync
meta-codesyncBot merged commit 3cc1ba9 into pytorch:mainMay 14, 2026
168 of 177 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@psiddh@kirklandsign
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229) - #19229

Merged
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2
May 14, 2026
Merged

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)#19229
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2

Conversation

@psiddh

@psiddhpsiddh commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Summary:
Combines all previously reverted Android improvement PRs (#18669, #19012, #19028, #19092, #19099, #19124) plus new Module lifecycle tests into a single atomic change.

Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.

Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.

Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.

JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).

Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.

This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.

Reviewed By: kirklandsign

CopilotAI review requested due to automatic review settings April 30, 2026 16:38
@pytorch-bot

pytorch-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/19229

Note: Links to docs will display an error until the docs builds have been completed.

❗ 1 Active SEVs

There are 1 currently active SEVs. If your PR is affected, please view them below:

❌ 2 New Failures, 4 Unrelated Failures

As of commit 85cc5e4 with merge base fe98297 (image):

NEW FAILURES - The following jobs have failed:

FLAKY - The following job failed but was likely due to flakiness present on trunk:

BROKEN TRUNK - The following jobs failed but was present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Apr 30, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Consolidates several Android API improvements across the ExecuTorch Java/Kotlin wrappers and JNI layer, focusing on exception-based error reporting, explicit lifecycle management (Closeable), and basic thread-safety guarantees, plus expanded instrumentation test coverage.

Changes:

  • Standardize synchronous error handling to throw exceptions (vs returning status codes / silent logging) across Module, LlmModule, training/ASR wrappers, and JNI glue.
  • Add/solidify lifecycle management via Closeable (close()/idempotent destroy patterns) and serialize access to non-thread-safe native state in LlmModule via ReentrantLock.
  • Update JNI registrations and Java native method naming (*Native), and expand Android instrumentation tests to cover lifecycle and API behavior.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaHandle Module.loadMethod() throwing by capturing an error code and early-returning safely; ensure destroy() in finally.
extension/android/jni/jni_layer_llama.cppAdd constructor exception-to-Java conversion; improve load() error reporting; rename registered natives (generateNative, loadNative, resetContextNative).
extension/android/jni/jni_layer.cppWrap native Module construction in exception mapping; throw on unsupported input EValue type codes; rename registered natives (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplement Closeable; replace silent failures with IllegalStateException on use-after-destroy.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaThrow IllegalStateException (vs generic RuntimeException) when optimizer is destroyed.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplement Closeable; add locking + destroyed checks; convert many status-returning APIs to void + exception; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktThrow ExecutorchRuntimeException with error code on creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplement Closeable; make loadMethod() throw; add locked wrappers around additional APIs; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdd error-code documentation, ALREADY_LOADED, improved message prefix, and cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaTighten file-path validation and switch to IllegalArgumentException.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignore and fix tests by providing required dummy input; add new API/lifecycle coverage.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdate tests for exception-based load; add close() lifecycle tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a96bd6b to 6ffdd62CompareApril 30, 2026 16:47
CopilotAI review requested due to automatic review settings April 30, 2026 17:05
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 787239e to 9f8afe1CompareApril 30, 2026 17:06
@psiddh
psiddh marked this pull request as draft April 30, 2026 17:10

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR consolidates several previously reverted Android improvements into one atomic update, standardizing ExecuTorch Android APIs around exception-based error reporting, explicit Closeable lifecycles, and improved thread-safety—along with significant new/updated instrumentation test coverage.

Changes:

  • Shift Android APIs away from status-code returns/silent failures to consistent exception throwing (Java/Kotlin + JNI).
  • Add/align Closeable lifecycle semantics (try-with-resources) and introduce locking around non-thread-safe native state (notably for LlmModule).
  • Update JNI registrations to match renamed *Native methods and expand Android instrumentation tests (including new lifecycle tests).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaAdapts benchmark load-path to exception-based Module.loadMethod() and ensures destroy on success path.
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.javaAdapts load-path to exception-based LlmModule.load() (but generate path still needs updates).
extension/android/jni/jni_layer_llama.cppAdds constructor error surfacing, improves load error reporting, and updates native method registrations (*Native).
extension/android/jni/jni_layer.cppAdds constructor error surfacing, throws on unsupported EValue input type, and updates native registrations (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplements Closeable and replaces silent failures with exceptions.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaUses IllegalStateException for destroyed optimizer usage.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplements Closeable, adds locking, converts sync APIs to throw-on-error wrappers, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktSwitches to ExecutorchRuntimeException for native creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplements Closeable, adds throw-on-error wrappers and lock-guarded public methods, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdds richer docs, new error code constant, improved message format, and a cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaMakes file-path validation stricter and consistently IllegalArgumentException-based.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignores/fixes Module tests and adds broader API/lifecycle coverage (load modes, log buffer, etdump, destroyed-state).
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdates to new throwing APIs and adds lifecycle tests (use-after-close, idempotent close).
Comments suppressed due to low confidence (1)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:102

  • LlmModule.generate(...) now throws on failure, but the MESSAGE_GENERATE branch doesn't catch exceptions. An exception here will crash the HandlerThread and can prevent onGenerationStopped() from running / results from being recorded. Wrap the generate call in try/catch (ideally extracting an error code from ExecutorchRuntimeException) and route the failure to the callback (e.g., call onGenerationStopped() and/or add an explicit error callback).
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has imported this pull request. If you are a Meta employee, you can view this in D103233465.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a44e428 to 04727f0CompareApril 30, 2026 18:19
@meta-codesyncmeta-codesyncBot changed the title Android: Closeable lifecycle, error reporting, thread safety, and tes…Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)May 5, 2026
psiddh added a commit to psiddh/executorch that referenced this pull request May 5, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 9adb92f to 274552bCompareMay 5, 2026 16:13
@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has exported this pull request. If you are a Meta employee, you can view the originating Diff in D103233465.

@psiddh
psiddh marked this pull request as ready for review May 5, 2026 16:18
CopilotAI review requested due to automatic review settings May 5, 2026 16:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:103

  • LlmModule.generate(...) now throws on failure (it no longer returns a status code). In the MESSAGE_GENERATE path, any exception will escape handleMessage, potentially killing the handler thread and skipping onGenerationStopped(). Wrap generate() in try/catch and ensure the callback is notified (and/or propagate an error status) instead of letting the background thread crash.
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.kt:62

  • These tests create a new LlmModule in setUp() for every test case but never close it, so native resources can accumulate across the suite (and potentially cause flakiness/OOM). Add an @After that calls llmModule.close() (or wrap each test body in try/finally) so the module is always released.
 llmModule =
LlmModule(getTestFilePath(TEST_FILE_NAME), getTestFilePath(TOKENIZER_FILE_NAME), 0.0f)
}
@Test
@Throws(IOException::class, URISyntaxException::class)
fun testGenerate() {
llmModule.load()
llmModule.generate(TEST_PROMPT, SEQ_LEN, this@LlmModuleInstrumentationTest)
assertEquals(results.size.toLong(), SEQ_LEN.toLong())
assertTrue(tokensPerSecond[tokensPerSecond.size - 1] > 0)
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/android/jni/jni_layer.cpp
@psiddh

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR and the comments

@claude

claudeBot commented May 8, 2026

Copy link
Copy Markdown

Claude finished @psiddh's task in 2m 14s —— View job


PR Review: Android Closeable lifecycle, error reporting, thread safety, and test coverage

  • Gather context and understand the request
  • Review core Java/Kotlin changes (Module, LlmModule, error handling)
  • Review JNI layer changes
  • Review benchmark/caller updates
  • Review test coverage
  • Assess existing review comments and provide consolidated feedback

Overall Assessment

This is a well-structured consolidation of several improvements. The move from status-code returns to exception-based error reporting is the right direction for Android APIs, and the Closeable lifecycle + ReentrantLock serialization are meaningful safety improvements. The test coverage additions are solid.

That said, there are a few issues worth addressing — some flagged by Copilot that I agree with, and some additional ones.


Issues I'd Recommend Fixing

1. stop() has no use-after-close guard (High Priority)

File:LlmModule.java:782

stop() is a bare public native void stop() with no mDestroyed check. Every other public method checks mDestroyed and throws IllegalStateException, but stop() would call into released native memory after close(). Since stop() intentionally skips the lock (it uses a C++ atomic flag), a simple volatile check is sufficient:

publicvoidstop() {
if (mDestroyed) thrownewIllegalStateException("LlmModule has been destroyed");
stopNative();
}
@DoNotStripprivatenativevoidstopNative();

This also requires updating the JNI registration in jni_layer_llama.cpp to map "stopNative"ExecuTorchLlmJni::stop.

Fix this →

2. Missing std::move for etdump_gen in profiling build (Medium Priority)

File:jni_layer.cpp:288-289

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, etdump_gen);

The Module constructor takes std::unique_ptr<runtime::EventTracer> by value. In the EXECUTORCH_ANDROID_PROFILING path, etdump_gen is a std::unique_ptr<ETDumpGen> — passing it without std::move won't compile. This should be:

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, std::move(etdump_gen));

The non-profiling path (auto etdump_gen = nullptr) works fine since nullptr implicitly converts to a null unique_ptr.

Fix this →

3. LlmModelRunner generate path lacks exception handling (Medium Priority)

File:LlmModelRunner.java:101-103

} elseif (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

generate() now throws ExecutorchRuntimeException on failure, but this path doesn't catch it. An exception here kills the HandlerThread and skips onGenerationStopped(). Should be wrapped like the load path:

} elseif (msg.what == MESSAGE_GENERATE) {
try {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
} catch (Exceptione) {
// Optionally surface error to callback
}
mLlmModelRunner.mCallback.onGenerationStopped();
}

Fix this →

4. LlmModuleInstrumentationTest leaks native resources (Low Priority)

File:LlmModuleInstrumentationTest.kt

setUp() creates a new LlmModule for each test but there's no @After method calling close(). Tests that don't explicitly close (e.g., testGenerate, testGenerateAndStop, all the prefill validation tests) leak native memory. Add:

@After
funtearDown() {
if (::llmModule.isInitialized) {
llmModule.close()
}
}

Minor Observations (Non-blocking)

5. testMethodMetadata() is a no-op test

File:ModuleInstrumentationTest.kt:56-59

funtestMethodMetadata() {
val module =Module.load(getTestFilePath(TEST_FILE_NAME))
module.destroy()
}

This test loads and immediately destroys without asserting anything about metadata. The newer testGetMethodMetadata() at line 216 does the actual work. The old test is dead code — consider removing it.

6. testNonPteFile doesn't clean up on unexpected success

File:ModuleInstrumentationTest.kt:112-117

funtestNonPteFile() {
val module =Module.load(getTestFilePath(NON_PTE_FILE_NAME))
module.loadMethod(FORWARD_METHOD) // expected to throw
}

If loadMethod doesn't throw (test expectation fails), the module is never destroyed. Consider using assertThrows with a finally block for cleanup.

7. ReentrantLock callback re-entrancy in LlmModule

File:LlmModule.java:322-331

The generate() method holds the ReentrantLock while native code invokes LlmCallback methods. Since the lock is reentrant, callbacks running on the same thread could re-enter other LlmModule methods (e.g., resetContext(), close()). The close() has a getHoldCount() > 1 guard, but other methods don't. This is an inherent limitation of using ReentrantLock — documenting that callbacks must not call back into LlmModule (except stop()) would help.


What Looks Good

  • ExecutorchRuntimeException: The error code mapping with human-readable messages and hex codes is excellent for debugging. The makeExecutorchException factory pattern is clean.
  • ExecuTorchRuntime.validateFilePath: Clear, specific error messages for each failure case (null, doesn't exist, not a file, not readable). This addresses the earlier Copilot feedback about !! — the current implementation is already clean.
  • Module.close()/destroy() pattern: Using tryLock() to detect concurrent execution and throwing rather than blocking is the right call for lifecycle methods.
  • TrainingModule: Simple and correct — checkNotDestroyed() + Closeable with idempotent close().
  • AsrModule: Good use of AtomicLong + getAndSet(0L) for thread-safe idempotent close without needing a lock.
  • JNI error surfacing: Both jni_layer.cpp and jni_layer_llama.cpp now catch native exceptions during construction and convert them to Java exceptions — this was a significant gap before.
  • Test coverage: The multi-threaded forward test, destroyed-state tests for all public methods, and the LLM lifecycle tests provide good confidence.

Summary

The PR is in good shape overall. The 3 higher-priority items (#1 stop() guard, #2 std::move, #3 generate exception handling) are worth fixing before merge as they represent potential crashes or UB. The rest are polish items.


CopilotAI review requested due to automatic review settings May 12, 2026 21:39
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 4558d37 to 0415dd1CompareMay 12, 2026 21:46

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment threadextension/android/jni/jni_layer.cpp
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 0415dd1 to e72eecdCompareMay 12, 2026 21:55
psiddh added a commit to psiddh/executorch that referenced this pull request May 13, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
CopilotAI review requested due to automatic review settings May 13, 2026 06:04
@psiddh
psiddhforce-pushed the android-combined-v2 branch from e72eecd to 881a8d5CompareMay 13, 2026 06:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 881a8d5 to 85cc5e4CompareMay 13, 2026 07:56
@meta-codesync
meta-codesyncBot merged commit 3cc1ba9 into pytorch:mainMay 14, 2026
168 of 177 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@psiddh@kirklandsign
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229) - #19229

Merged
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2
May 14, 2026
Merged

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)#19229
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2

Conversation

@psiddh

@psiddhpsiddh commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Summary:
Combines all previously reverted Android improvement PRs (#18669, #19012, #19028, #19092, #19099, #19124) plus new Module lifecycle tests into a single atomic change.

Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.

Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.

Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.

JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).

Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.

This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.

Reviewed By: kirklandsign

CopilotAI review requested due to automatic review settings April 30, 2026 16:38
@pytorch-bot

pytorch-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/19229

Note: Links to docs will display an error until the docs builds have been completed.

❗ 1 Active SEVs

There are 1 currently active SEVs. If your PR is affected, please view them below:

❌ 2 New Failures, 4 Unrelated Failures

As of commit 85cc5e4 with merge base fe98297 (image):

NEW FAILURES - The following jobs have failed:

FLAKY - The following job failed but was likely due to flakiness present on trunk:

BROKEN TRUNK - The following jobs failed but was present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Apr 30, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Consolidates several Android API improvements across the ExecuTorch Java/Kotlin wrappers and JNI layer, focusing on exception-based error reporting, explicit lifecycle management (Closeable), and basic thread-safety guarantees, plus expanded instrumentation test coverage.

Changes:

  • Standardize synchronous error handling to throw exceptions (vs returning status codes / silent logging) across Module, LlmModule, training/ASR wrappers, and JNI glue.
  • Add/solidify lifecycle management via Closeable (close()/idempotent destroy patterns) and serialize access to non-thread-safe native state in LlmModule via ReentrantLock.
  • Update JNI registrations and Java native method naming (*Native), and expand Android instrumentation tests to cover lifecycle and API behavior.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaHandle Module.loadMethod() throwing by capturing an error code and early-returning safely; ensure destroy() in finally.
extension/android/jni/jni_layer_llama.cppAdd constructor exception-to-Java conversion; improve load() error reporting; rename registered natives (generateNative, loadNative, resetContextNative).
extension/android/jni/jni_layer.cppWrap native Module construction in exception mapping; throw on unsupported input EValue type codes; rename registered natives (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplement Closeable; replace silent failures with IllegalStateException on use-after-destroy.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaThrow IllegalStateException (vs generic RuntimeException) when optimizer is destroyed.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplement Closeable; add locking + destroyed checks; convert many status-returning APIs to void + exception; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktThrow ExecutorchRuntimeException with error code on creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplement Closeable; make loadMethod() throw; add locked wrappers around additional APIs; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdd error-code documentation, ALREADY_LOADED, improved message prefix, and cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaTighten file-path validation and switch to IllegalArgumentException.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignore and fix tests by providing required dummy input; add new API/lifecycle coverage.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdate tests for exception-based load; add close() lifecycle tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a96bd6b to 6ffdd62CompareApril 30, 2026 16:47
CopilotAI review requested due to automatic review settings April 30, 2026 17:05
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 787239e to 9f8afe1CompareApril 30, 2026 17:06
@psiddh
psiddh marked this pull request as draft April 30, 2026 17:10

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR consolidates several previously reverted Android improvements into one atomic update, standardizing ExecuTorch Android APIs around exception-based error reporting, explicit Closeable lifecycles, and improved thread-safety—along with significant new/updated instrumentation test coverage.

Changes:

  • Shift Android APIs away from status-code returns/silent failures to consistent exception throwing (Java/Kotlin + JNI).
  • Add/align Closeable lifecycle semantics (try-with-resources) and introduce locking around non-thread-safe native state (notably for LlmModule).
  • Update JNI registrations to match renamed *Native methods and expand Android instrumentation tests (including new lifecycle tests).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaAdapts benchmark load-path to exception-based Module.loadMethod() and ensures destroy on success path.
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.javaAdapts load-path to exception-based LlmModule.load() (but generate path still needs updates).
extension/android/jni/jni_layer_llama.cppAdds constructor error surfacing, improves load error reporting, and updates native method registrations (*Native).
extension/android/jni/jni_layer.cppAdds constructor error surfacing, throws on unsupported EValue input type, and updates native registrations (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplements Closeable and replaces silent failures with exceptions.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaUses IllegalStateException for destroyed optimizer usage.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplements Closeable, adds locking, converts sync APIs to throw-on-error wrappers, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktSwitches to ExecutorchRuntimeException for native creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplements Closeable, adds throw-on-error wrappers and lock-guarded public methods, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdds richer docs, new error code constant, improved message format, and a cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaMakes file-path validation stricter and consistently IllegalArgumentException-based.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignores/fixes Module tests and adds broader API/lifecycle coverage (load modes, log buffer, etdump, destroyed-state).
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdates to new throwing APIs and adds lifecycle tests (use-after-close, idempotent close).
Comments suppressed due to low confidence (1)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:102

  • LlmModule.generate(...) now throws on failure, but the MESSAGE_GENERATE branch doesn't catch exceptions. An exception here will crash the HandlerThread and can prevent onGenerationStopped() from running / results from being recorded. Wrap the generate call in try/catch (ideally extracting an error code from ExecutorchRuntimeException) and route the failure to the callback (e.g., call onGenerationStopped() and/or add an explicit error callback).
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has imported this pull request. If you are a Meta employee, you can view this in D103233465.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a44e428 to 04727f0CompareApril 30, 2026 18:19
@meta-codesyncmeta-codesyncBot changed the title Android: Closeable lifecycle, error reporting, thread safety, and tes…Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)May 5, 2026
psiddh added a commit to psiddh/executorch that referenced this pull request May 5, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 9adb92f to 274552bCompareMay 5, 2026 16:13
@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has exported this pull request. If you are a Meta employee, you can view the originating Diff in D103233465.

@psiddh
psiddh marked this pull request as ready for review May 5, 2026 16:18
CopilotAI review requested due to automatic review settings May 5, 2026 16:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:103

  • LlmModule.generate(...) now throws on failure (it no longer returns a status code). In the MESSAGE_GENERATE path, any exception will escape handleMessage, potentially killing the handler thread and skipping onGenerationStopped(). Wrap generate() in try/catch and ensure the callback is notified (and/or propagate an error status) instead of letting the background thread crash.
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.kt:62

  • These tests create a new LlmModule in setUp() for every test case but never close it, so native resources can accumulate across the suite (and potentially cause flakiness/OOM). Add an @After that calls llmModule.close() (or wrap each test body in try/finally) so the module is always released.
 llmModule =
LlmModule(getTestFilePath(TEST_FILE_NAME), getTestFilePath(TOKENIZER_FILE_NAME), 0.0f)
}
@Test
@Throws(IOException::class, URISyntaxException::class)
fun testGenerate() {
llmModule.load()
llmModule.generate(TEST_PROMPT, SEQ_LEN, this@LlmModuleInstrumentationTest)
assertEquals(results.size.toLong(), SEQ_LEN.toLong())
assertTrue(tokensPerSecond[tokensPerSecond.size - 1] > 0)
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/android/jni/jni_layer.cpp
@psiddh

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR and the comments

@claude

claudeBot commented May 8, 2026

Copy link
Copy Markdown

Claude finished @psiddh's task in 2m 14s —— View job


PR Review: Android Closeable lifecycle, error reporting, thread safety, and test coverage

  • Gather context and understand the request
  • Review core Java/Kotlin changes (Module, LlmModule, error handling)
  • Review JNI layer changes
  • Review benchmark/caller updates
  • Review test coverage
  • Assess existing review comments and provide consolidated feedback

Overall Assessment

This is a well-structured consolidation of several improvements. The move from status-code returns to exception-based error reporting is the right direction for Android APIs, and the Closeable lifecycle + ReentrantLock serialization are meaningful safety improvements. The test coverage additions are solid.

That said, there are a few issues worth addressing — some flagged by Copilot that I agree with, and some additional ones.


Issues I'd Recommend Fixing

1. stop() has no use-after-close guard (High Priority)

File:LlmModule.java:782

stop() is a bare public native void stop() with no mDestroyed check. Every other public method checks mDestroyed and throws IllegalStateException, but stop() would call into released native memory after close(). Since stop() intentionally skips the lock (it uses a C++ atomic flag), a simple volatile check is sufficient:

publicvoidstop() {
if (mDestroyed) thrownewIllegalStateException("LlmModule has been destroyed");
stopNative();
}
@DoNotStripprivatenativevoidstopNative();

This also requires updating the JNI registration in jni_layer_llama.cpp to map "stopNative"ExecuTorchLlmJni::stop.

Fix this →

2. Missing std::move for etdump_gen in profiling build (Medium Priority)

File:jni_layer.cpp:288-289

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, etdump_gen);

The Module constructor takes std::unique_ptr<runtime::EventTracer> by value. In the EXECUTORCH_ANDROID_PROFILING path, etdump_gen is a std::unique_ptr<ETDumpGen> — passing it without std::move won't compile. This should be:

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, std::move(etdump_gen));

The non-profiling path (auto etdump_gen = nullptr) works fine since nullptr implicitly converts to a null unique_ptr.

Fix this →

3. LlmModelRunner generate path lacks exception handling (Medium Priority)

File:LlmModelRunner.java:101-103

} elseif (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

generate() now throws ExecutorchRuntimeException on failure, but this path doesn't catch it. An exception here kills the HandlerThread and skips onGenerationStopped(). Should be wrapped like the load path:

} elseif (msg.what == MESSAGE_GENERATE) {
try {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
} catch (Exceptione) {
// Optionally surface error to callback
}
mLlmModelRunner.mCallback.onGenerationStopped();
}

Fix this →

4. LlmModuleInstrumentationTest leaks native resources (Low Priority)

File:LlmModuleInstrumentationTest.kt

setUp() creates a new LlmModule for each test but there's no @After method calling close(). Tests that don't explicitly close (e.g., testGenerate, testGenerateAndStop, all the prefill validation tests) leak native memory. Add:

@After
funtearDown() {
if (::llmModule.isInitialized) {
llmModule.close()
}
}

Minor Observations (Non-blocking)

5. testMethodMetadata() is a no-op test

File:ModuleInstrumentationTest.kt:56-59

funtestMethodMetadata() {
val module =Module.load(getTestFilePath(TEST_FILE_NAME))
module.destroy()
}

This test loads and immediately destroys without asserting anything about metadata. The newer testGetMethodMetadata() at line 216 does the actual work. The old test is dead code — consider removing it.

6. testNonPteFile doesn't clean up on unexpected success

File:ModuleInstrumentationTest.kt:112-117

funtestNonPteFile() {
val module =Module.load(getTestFilePath(NON_PTE_FILE_NAME))
module.loadMethod(FORWARD_METHOD) // expected to throw
}

If loadMethod doesn't throw (test expectation fails), the module is never destroyed. Consider using assertThrows with a finally block for cleanup.

7. ReentrantLock callback re-entrancy in LlmModule

File:LlmModule.java:322-331

The generate() method holds the ReentrantLock while native code invokes LlmCallback methods. Since the lock is reentrant, callbacks running on the same thread could re-enter other LlmModule methods (e.g., resetContext(), close()). The close() has a getHoldCount() > 1 guard, but other methods don't. This is an inherent limitation of using ReentrantLock — documenting that callbacks must not call back into LlmModule (except stop()) would help.


What Looks Good

  • ExecutorchRuntimeException: The error code mapping with human-readable messages and hex codes is excellent for debugging. The makeExecutorchException factory pattern is clean.
  • ExecuTorchRuntime.validateFilePath: Clear, specific error messages for each failure case (null, doesn't exist, not a file, not readable). This addresses the earlier Copilot feedback about !! — the current implementation is already clean.
  • Module.close()/destroy() pattern: Using tryLock() to detect concurrent execution and throwing rather than blocking is the right call for lifecycle methods.
  • TrainingModule: Simple and correct — checkNotDestroyed() + Closeable with idempotent close().
  • AsrModule: Good use of AtomicLong + getAndSet(0L) for thread-safe idempotent close without needing a lock.
  • JNI error surfacing: Both jni_layer.cpp and jni_layer_llama.cpp now catch native exceptions during construction and convert them to Java exceptions — this was a significant gap before.
  • Test coverage: The multi-threaded forward test, destroyed-state tests for all public methods, and the LLM lifecycle tests provide good confidence.

Summary

The PR is in good shape overall. The 3 higher-priority items (#1 stop() guard, #2 std::move, #3 generate exception handling) are worth fixing before merge as they represent potential crashes or UB. The rest are polish items.


CopilotAI review requested due to automatic review settings May 12, 2026 21:39
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 4558d37 to 0415dd1CompareMay 12, 2026 21:46

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment threadextension/android/jni/jni_layer.cpp
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 0415dd1 to e72eecdCompareMay 12, 2026 21:55
psiddh added a commit to psiddh/executorch that referenced this pull request May 13, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
CopilotAI review requested due to automatic review settings May 13, 2026 06:04
@psiddh
psiddhforce-pushed the android-combined-v2 branch from e72eecd to 881a8d5CompareMay 13, 2026 06:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 881a8d5 to 85cc5e4CompareMay 13, 2026 07:56
@meta-codesync
meta-codesyncBot merged commit 3cc1ba9 into pytorch:mainMay 14, 2026
168 of 177 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@psiddh@kirklandsign
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229) - #19229

Merged
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2
May 14, 2026
Merged

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)#19229
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2

Conversation

@psiddh

@psiddhpsiddh commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Summary:
Combines all previously reverted Android improvement PRs (#18669, #19012, #19028, #19092, #19099, #19124) plus new Module lifecycle tests into a single atomic change.

Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.

Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.

Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.

JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).

Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.

This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.

Reviewed By: kirklandsign

CopilotAI review requested due to automatic review settings April 30, 2026 16:38
@pytorch-bot

pytorch-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/19229

Note: Links to docs will display an error until the docs builds have been completed.

❗ 1 Active SEVs

There are 1 currently active SEVs. If your PR is affected, please view them below:

❌ 2 New Failures, 4 Unrelated Failures

As of commit 85cc5e4 with merge base fe98297 (image):

NEW FAILURES - The following jobs have failed:

FLAKY - The following job failed but was likely due to flakiness present on trunk:

BROKEN TRUNK - The following jobs failed but was present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Apr 30, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Consolidates several Android API improvements across the ExecuTorch Java/Kotlin wrappers and JNI layer, focusing on exception-based error reporting, explicit lifecycle management (Closeable), and basic thread-safety guarantees, plus expanded instrumentation test coverage.

Changes:

  • Standardize synchronous error handling to throw exceptions (vs returning status codes / silent logging) across Module, LlmModule, training/ASR wrappers, and JNI glue.
  • Add/solidify lifecycle management via Closeable (close()/idempotent destroy patterns) and serialize access to non-thread-safe native state in LlmModule via ReentrantLock.
  • Update JNI registrations and Java native method naming (*Native), and expand Android instrumentation tests to cover lifecycle and API behavior.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaHandle Module.loadMethod() throwing by capturing an error code and early-returning safely; ensure destroy() in finally.
extension/android/jni/jni_layer_llama.cppAdd constructor exception-to-Java conversion; improve load() error reporting; rename registered natives (generateNative, loadNative, resetContextNative).
extension/android/jni/jni_layer.cppWrap native Module construction in exception mapping; throw on unsupported input EValue type codes; rename registered natives (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplement Closeable; replace silent failures with IllegalStateException on use-after-destroy.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaThrow IllegalStateException (vs generic RuntimeException) when optimizer is destroyed.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplement Closeable; add locking + destroyed checks; convert many status-returning APIs to void + exception; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktThrow ExecutorchRuntimeException with error code on creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplement Closeable; make loadMethod() throw; add locked wrappers around additional APIs; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdd error-code documentation, ALREADY_LOADED, improved message prefix, and cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaTighten file-path validation and switch to IllegalArgumentException.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignore and fix tests by providing required dummy input; add new API/lifecycle coverage.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdate tests for exception-based load; add close() lifecycle tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a96bd6b to 6ffdd62CompareApril 30, 2026 16:47
CopilotAI review requested due to automatic review settings April 30, 2026 17:05
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 787239e to 9f8afe1CompareApril 30, 2026 17:06
@psiddh
psiddh marked this pull request as draft April 30, 2026 17:10

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR consolidates several previously reverted Android improvements into one atomic update, standardizing ExecuTorch Android APIs around exception-based error reporting, explicit Closeable lifecycles, and improved thread-safety—along with significant new/updated instrumentation test coverage.

Changes:

  • Shift Android APIs away from status-code returns/silent failures to consistent exception throwing (Java/Kotlin + JNI).
  • Add/align Closeable lifecycle semantics (try-with-resources) and introduce locking around non-thread-safe native state (notably for LlmModule).
  • Update JNI registrations to match renamed *Native methods and expand Android instrumentation tests (including new lifecycle tests).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaAdapts benchmark load-path to exception-based Module.loadMethod() and ensures destroy on success path.
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.javaAdapts load-path to exception-based LlmModule.load() (but generate path still needs updates).
extension/android/jni/jni_layer_llama.cppAdds constructor error surfacing, improves load error reporting, and updates native method registrations (*Native).
extension/android/jni/jni_layer.cppAdds constructor error surfacing, throws on unsupported EValue input type, and updates native registrations (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplements Closeable and replaces silent failures with exceptions.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaUses IllegalStateException for destroyed optimizer usage.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplements Closeable, adds locking, converts sync APIs to throw-on-error wrappers, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktSwitches to ExecutorchRuntimeException for native creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplements Closeable, adds throw-on-error wrappers and lock-guarded public methods, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdds richer docs, new error code constant, improved message format, and a cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaMakes file-path validation stricter and consistently IllegalArgumentException-based.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignores/fixes Module tests and adds broader API/lifecycle coverage (load modes, log buffer, etdump, destroyed-state).
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdates to new throwing APIs and adds lifecycle tests (use-after-close, idempotent close).
Comments suppressed due to low confidence (1)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:102

  • LlmModule.generate(...) now throws on failure, but the MESSAGE_GENERATE branch doesn't catch exceptions. An exception here will crash the HandlerThread and can prevent onGenerationStopped() from running / results from being recorded. Wrap the generate call in try/catch (ideally extracting an error code from ExecutorchRuntimeException) and route the failure to the callback (e.g., call onGenerationStopped() and/or add an explicit error callback).
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has imported this pull request. If you are a Meta employee, you can view this in D103233465.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a44e428 to 04727f0CompareApril 30, 2026 18:19
@meta-codesyncmeta-codesyncBot changed the title Android: Closeable lifecycle, error reporting, thread safety, and tes…Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)May 5, 2026
psiddh added a commit to psiddh/executorch that referenced this pull request May 5, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 9adb92f to 274552bCompareMay 5, 2026 16:13
@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has exported this pull request. If you are a Meta employee, you can view the originating Diff in D103233465.

@psiddh
psiddh marked this pull request as ready for review May 5, 2026 16:18
CopilotAI review requested due to automatic review settings May 5, 2026 16:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:103

  • LlmModule.generate(...) now throws on failure (it no longer returns a status code). In the MESSAGE_GENERATE path, any exception will escape handleMessage, potentially killing the handler thread and skipping onGenerationStopped(). Wrap generate() in try/catch and ensure the callback is notified (and/or propagate an error status) instead of letting the background thread crash.
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.kt:62

  • These tests create a new LlmModule in setUp() for every test case but never close it, so native resources can accumulate across the suite (and potentially cause flakiness/OOM). Add an @After that calls llmModule.close() (or wrap each test body in try/finally) so the module is always released.
 llmModule =
LlmModule(getTestFilePath(TEST_FILE_NAME), getTestFilePath(TOKENIZER_FILE_NAME), 0.0f)
}
@Test
@Throws(IOException::class, URISyntaxException::class)
fun testGenerate() {
llmModule.load()
llmModule.generate(TEST_PROMPT, SEQ_LEN, this@LlmModuleInstrumentationTest)
assertEquals(results.size.toLong(), SEQ_LEN.toLong())
assertTrue(tokensPerSecond[tokensPerSecond.size - 1] > 0)
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/android/jni/jni_layer.cpp
@psiddh

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR and the comments

@claude

claudeBot commented May 8, 2026

Copy link
Copy Markdown

Claude finished @psiddh's task in 2m 14s —— View job


PR Review: Android Closeable lifecycle, error reporting, thread safety, and test coverage

  • Gather context and understand the request
  • Review core Java/Kotlin changes (Module, LlmModule, error handling)
  • Review JNI layer changes
  • Review benchmark/caller updates
  • Review test coverage
  • Assess existing review comments and provide consolidated feedback

Overall Assessment

This is a well-structured consolidation of several improvements. The move from status-code returns to exception-based error reporting is the right direction for Android APIs, and the Closeable lifecycle + ReentrantLock serialization are meaningful safety improvements. The test coverage additions are solid.

That said, there are a few issues worth addressing — some flagged by Copilot that I agree with, and some additional ones.


Issues I'd Recommend Fixing

1. stop() has no use-after-close guard (High Priority)

File:LlmModule.java:782

stop() is a bare public native void stop() with no mDestroyed check. Every other public method checks mDestroyed and throws IllegalStateException, but stop() would call into released native memory after close(). Since stop() intentionally skips the lock (it uses a C++ atomic flag), a simple volatile check is sufficient:

publicvoidstop() {
if (mDestroyed) thrownewIllegalStateException("LlmModule has been destroyed");
stopNative();
}
@DoNotStripprivatenativevoidstopNative();

This also requires updating the JNI registration in jni_layer_llama.cpp to map "stopNative"ExecuTorchLlmJni::stop.

Fix this →

2. Missing std::move for etdump_gen in profiling build (Medium Priority)

File:jni_layer.cpp:288-289

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, etdump_gen);

The Module constructor takes std::unique_ptr<runtime::EventTracer> by value. In the EXECUTORCH_ANDROID_PROFILING path, etdump_gen is a std::unique_ptr<ETDumpGen> — passing it without std::move won't compile. This should be:

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, std::move(etdump_gen));

The non-profiling path (auto etdump_gen = nullptr) works fine since nullptr implicitly converts to a null unique_ptr.

Fix this →

3. LlmModelRunner generate path lacks exception handling (Medium Priority)

File:LlmModelRunner.java:101-103

} elseif (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

generate() now throws ExecutorchRuntimeException on failure, but this path doesn't catch it. An exception here kills the HandlerThread and skips onGenerationStopped(). Should be wrapped like the load path:

} elseif (msg.what == MESSAGE_GENERATE) {
try {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
} catch (Exceptione) {
// Optionally surface error to callback
}
mLlmModelRunner.mCallback.onGenerationStopped();
}

Fix this →

4. LlmModuleInstrumentationTest leaks native resources (Low Priority)

File:LlmModuleInstrumentationTest.kt

setUp() creates a new LlmModule for each test but there's no @After method calling close(). Tests that don't explicitly close (e.g., testGenerate, testGenerateAndStop, all the prefill validation tests) leak native memory. Add:

@After
funtearDown() {
if (::llmModule.isInitialized) {
llmModule.close()
}
}

Minor Observations (Non-blocking)

5. testMethodMetadata() is a no-op test

File:ModuleInstrumentationTest.kt:56-59

funtestMethodMetadata() {
val module =Module.load(getTestFilePath(TEST_FILE_NAME))
module.destroy()
}

This test loads and immediately destroys without asserting anything about metadata. The newer testGetMethodMetadata() at line 216 does the actual work. The old test is dead code — consider removing it.

6. testNonPteFile doesn't clean up on unexpected success

File:ModuleInstrumentationTest.kt:112-117

funtestNonPteFile() {
val module =Module.load(getTestFilePath(NON_PTE_FILE_NAME))
module.loadMethod(FORWARD_METHOD) // expected to throw
}

If loadMethod doesn't throw (test expectation fails), the module is never destroyed. Consider using assertThrows with a finally block for cleanup.

7. ReentrantLock callback re-entrancy in LlmModule

File:LlmModule.java:322-331

The generate() method holds the ReentrantLock while native code invokes LlmCallback methods. Since the lock is reentrant, callbacks running on the same thread could re-enter other LlmModule methods (e.g., resetContext(), close()). The close() has a getHoldCount() > 1 guard, but other methods don't. This is an inherent limitation of using ReentrantLock — documenting that callbacks must not call back into LlmModule (except stop()) would help.


What Looks Good

  • ExecutorchRuntimeException: The error code mapping with human-readable messages and hex codes is excellent for debugging. The makeExecutorchException factory pattern is clean.
  • ExecuTorchRuntime.validateFilePath: Clear, specific error messages for each failure case (null, doesn't exist, not a file, not readable). This addresses the earlier Copilot feedback about !! — the current implementation is already clean.
  • Module.close()/destroy() pattern: Using tryLock() to detect concurrent execution and throwing rather than blocking is the right call for lifecycle methods.
  • TrainingModule: Simple and correct — checkNotDestroyed() + Closeable with idempotent close().
  • AsrModule: Good use of AtomicLong + getAndSet(0L) for thread-safe idempotent close without needing a lock.
  • JNI error surfacing: Both jni_layer.cpp and jni_layer_llama.cpp now catch native exceptions during construction and convert them to Java exceptions — this was a significant gap before.
  • Test coverage: The multi-threaded forward test, destroyed-state tests for all public methods, and the LLM lifecycle tests provide good confidence.

Summary

The PR is in good shape overall. The 3 higher-priority items (#1 stop() guard, #2 std::move, #3 generate exception handling) are worth fixing before merge as they represent potential crashes or UB. The rest are polish items.


CopilotAI review requested due to automatic review settings May 12, 2026 21:39
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 4558d37 to 0415dd1CompareMay 12, 2026 21:46

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment threadextension/android/jni/jni_layer.cpp
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 0415dd1 to e72eecdCompareMay 12, 2026 21:55
psiddh added a commit to psiddh/executorch that referenced this pull request May 13, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
CopilotAI review requested due to automatic review settings May 13, 2026 06:04
@psiddh
psiddhforce-pushed the android-combined-v2 branch from e72eecd to 881a8d5CompareMay 13, 2026 06:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 881a8d5 to 85cc5e4CompareMay 13, 2026 07:56
@meta-codesync
meta-codesyncBot merged commit 3cc1ba9 into pytorch:mainMay 14, 2026
168 of 177 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@psiddh@kirklandsign
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229) - #19229

Merged
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2
May 14, 2026
Merged

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)#19229
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2

Conversation

@psiddh

@psiddhpsiddh commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Summary:
Combines all previously reverted Android improvement PRs (#18669, #19012, #19028, #19092, #19099, #19124) plus new Module lifecycle tests into a single atomic change.

Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.

Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.

Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.

JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).

Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.

This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.

Reviewed By: kirklandsign

CopilotAI review requested due to automatic review settings April 30, 2026 16:38
@pytorch-bot

pytorch-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/19229

Note: Links to docs will display an error until the docs builds have been completed.

❗ 1 Active SEVs

There are 1 currently active SEVs. If your PR is affected, please view them below:

❌ 2 New Failures, 4 Unrelated Failures

As of commit 85cc5e4 with merge base fe98297 (image):

NEW FAILURES - The following jobs have failed:

FLAKY - The following job failed but was likely due to flakiness present on trunk:

BROKEN TRUNK - The following jobs failed but was present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Apr 30, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Consolidates several Android API improvements across the ExecuTorch Java/Kotlin wrappers and JNI layer, focusing on exception-based error reporting, explicit lifecycle management (Closeable), and basic thread-safety guarantees, plus expanded instrumentation test coverage.

Changes:

  • Standardize synchronous error handling to throw exceptions (vs returning status codes / silent logging) across Module, LlmModule, training/ASR wrappers, and JNI glue.
  • Add/solidify lifecycle management via Closeable (close()/idempotent destroy patterns) and serialize access to non-thread-safe native state in LlmModule via ReentrantLock.
  • Update JNI registrations and Java native method naming (*Native), and expand Android instrumentation tests to cover lifecycle and API behavior.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaHandle Module.loadMethod() throwing by capturing an error code and early-returning safely; ensure destroy() in finally.
extension/android/jni/jni_layer_llama.cppAdd constructor exception-to-Java conversion; improve load() error reporting; rename registered natives (generateNative, loadNative, resetContextNative).
extension/android/jni/jni_layer.cppWrap native Module construction in exception mapping; throw on unsupported input EValue type codes; rename registered natives (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplement Closeable; replace silent failures with IllegalStateException on use-after-destroy.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaThrow IllegalStateException (vs generic RuntimeException) when optimizer is destroyed.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplement Closeable; add locking + destroyed checks; convert many status-returning APIs to void + exception; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktThrow ExecutorchRuntimeException with error code on creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplement Closeable; make loadMethod() throw; add locked wrappers around additional APIs; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdd error-code documentation, ALREADY_LOADED, improved message prefix, and cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaTighten file-path validation and switch to IllegalArgumentException.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignore and fix tests by providing required dummy input; add new API/lifecycle coverage.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdate tests for exception-based load; add close() lifecycle tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a96bd6b to 6ffdd62CompareApril 30, 2026 16:47
CopilotAI review requested due to automatic review settings April 30, 2026 17:05
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 787239e to 9f8afe1CompareApril 30, 2026 17:06
@psiddh
psiddh marked this pull request as draft April 30, 2026 17:10

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR consolidates several previously reverted Android improvements into one atomic update, standardizing ExecuTorch Android APIs around exception-based error reporting, explicit Closeable lifecycles, and improved thread-safety—along with significant new/updated instrumentation test coverage.

Changes:

  • Shift Android APIs away from status-code returns/silent failures to consistent exception throwing (Java/Kotlin + JNI).
  • Add/align Closeable lifecycle semantics (try-with-resources) and introduce locking around non-thread-safe native state (notably for LlmModule).
  • Update JNI registrations to match renamed *Native methods and expand Android instrumentation tests (including new lifecycle tests).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaAdapts benchmark load-path to exception-based Module.loadMethod() and ensures destroy on success path.
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.javaAdapts load-path to exception-based LlmModule.load() (but generate path still needs updates).
extension/android/jni/jni_layer_llama.cppAdds constructor error surfacing, improves load error reporting, and updates native method registrations (*Native).
extension/android/jni/jni_layer.cppAdds constructor error surfacing, throws on unsupported EValue input type, and updates native registrations (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplements Closeable and replaces silent failures with exceptions.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaUses IllegalStateException for destroyed optimizer usage.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplements Closeable, adds locking, converts sync APIs to throw-on-error wrappers, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktSwitches to ExecutorchRuntimeException for native creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplements Closeable, adds throw-on-error wrappers and lock-guarded public methods, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdds richer docs, new error code constant, improved message format, and a cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaMakes file-path validation stricter and consistently IllegalArgumentException-based.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignores/fixes Module tests and adds broader API/lifecycle coverage (load modes, log buffer, etdump, destroyed-state).
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdates to new throwing APIs and adds lifecycle tests (use-after-close, idempotent close).
Comments suppressed due to low confidence (1)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:102

  • LlmModule.generate(...) now throws on failure, but the MESSAGE_GENERATE branch doesn't catch exceptions. An exception here will crash the HandlerThread and can prevent onGenerationStopped() from running / results from being recorded. Wrap the generate call in try/catch (ideally extracting an error code from ExecutorchRuntimeException) and route the failure to the callback (e.g., call onGenerationStopped() and/or add an explicit error callback).
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has imported this pull request. If you are a Meta employee, you can view this in D103233465.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a44e428 to 04727f0CompareApril 30, 2026 18:19
@meta-codesyncmeta-codesyncBot changed the title Android: Closeable lifecycle, error reporting, thread safety, and tes…Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)May 5, 2026
psiddh added a commit to psiddh/executorch that referenced this pull request May 5, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 9adb92f to 274552bCompareMay 5, 2026 16:13
@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has exported this pull request. If you are a Meta employee, you can view the originating Diff in D103233465.

@psiddh
psiddh marked this pull request as ready for review May 5, 2026 16:18
CopilotAI review requested due to automatic review settings May 5, 2026 16:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:103

  • LlmModule.generate(...) now throws on failure (it no longer returns a status code). In the MESSAGE_GENERATE path, any exception will escape handleMessage, potentially killing the handler thread and skipping onGenerationStopped(). Wrap generate() in try/catch and ensure the callback is notified (and/or propagate an error status) instead of letting the background thread crash.
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.kt:62

  • These tests create a new LlmModule in setUp() for every test case but never close it, so native resources can accumulate across the suite (and potentially cause flakiness/OOM). Add an @After that calls llmModule.close() (or wrap each test body in try/finally) so the module is always released.
 llmModule =
LlmModule(getTestFilePath(TEST_FILE_NAME), getTestFilePath(TOKENIZER_FILE_NAME), 0.0f)
}
@Test
@Throws(IOException::class, URISyntaxException::class)
fun testGenerate() {
llmModule.load()
llmModule.generate(TEST_PROMPT, SEQ_LEN, this@LlmModuleInstrumentationTest)
assertEquals(results.size.toLong(), SEQ_LEN.toLong())
assertTrue(tokensPerSecond[tokensPerSecond.size - 1] > 0)
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/android/jni/jni_layer.cpp
@psiddh

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR and the comments

@claude

claudeBot commented May 8, 2026

Copy link
Copy Markdown

Claude finished @psiddh's task in 2m 14s —— View job


PR Review: Android Closeable lifecycle, error reporting, thread safety, and test coverage

  • Gather context and understand the request
  • Review core Java/Kotlin changes (Module, LlmModule, error handling)
  • Review JNI layer changes
  • Review benchmark/caller updates
  • Review test coverage
  • Assess existing review comments and provide consolidated feedback

Overall Assessment

This is a well-structured consolidation of several improvements. The move from status-code returns to exception-based error reporting is the right direction for Android APIs, and the Closeable lifecycle + ReentrantLock serialization are meaningful safety improvements. The test coverage additions are solid.

That said, there are a few issues worth addressing — some flagged by Copilot that I agree with, and some additional ones.


Issues I'd Recommend Fixing

1. stop() has no use-after-close guard (High Priority)

File:LlmModule.java:782

stop() is a bare public native void stop() with no mDestroyed check. Every other public method checks mDestroyed and throws IllegalStateException, but stop() would call into released native memory after close(). Since stop() intentionally skips the lock (it uses a C++ atomic flag), a simple volatile check is sufficient:

publicvoidstop() {
if (mDestroyed) thrownewIllegalStateException("LlmModule has been destroyed");
stopNative();
}
@DoNotStripprivatenativevoidstopNative();

This also requires updating the JNI registration in jni_layer_llama.cpp to map "stopNative"ExecuTorchLlmJni::stop.

Fix this →

2. Missing std::move for etdump_gen in profiling build (Medium Priority)

File:jni_layer.cpp:288-289

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, etdump_gen);

The Module constructor takes std::unique_ptr<runtime::EventTracer> by value. In the EXECUTORCH_ANDROID_PROFILING path, etdump_gen is a std::unique_ptr<ETDumpGen> — passing it without std::move won't compile. This should be:

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, std::move(etdump_gen));

The non-profiling path (auto etdump_gen = nullptr) works fine since nullptr implicitly converts to a null unique_ptr.

Fix this →

3. LlmModelRunner generate path lacks exception handling (Medium Priority)

File:LlmModelRunner.java:101-103

} elseif (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

generate() now throws ExecutorchRuntimeException on failure, but this path doesn't catch it. An exception here kills the HandlerThread and skips onGenerationStopped(). Should be wrapped like the load path:

} elseif (msg.what == MESSAGE_GENERATE) {
try {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
} catch (Exceptione) {
// Optionally surface error to callback
}
mLlmModelRunner.mCallback.onGenerationStopped();
}

Fix this →

4. LlmModuleInstrumentationTest leaks native resources (Low Priority)

File:LlmModuleInstrumentationTest.kt

setUp() creates a new LlmModule for each test but there's no @After method calling close(). Tests that don't explicitly close (e.g., testGenerate, testGenerateAndStop, all the prefill validation tests) leak native memory. Add:

@After
funtearDown() {
if (::llmModule.isInitialized) {
llmModule.close()
}
}

Minor Observations (Non-blocking)

5. testMethodMetadata() is a no-op test

File:ModuleInstrumentationTest.kt:56-59

funtestMethodMetadata() {
val module =Module.load(getTestFilePath(TEST_FILE_NAME))
module.destroy()
}

This test loads and immediately destroys without asserting anything about metadata. The newer testGetMethodMetadata() at line 216 does the actual work. The old test is dead code — consider removing it.

6. testNonPteFile doesn't clean up on unexpected success

File:ModuleInstrumentationTest.kt:112-117

funtestNonPteFile() {
val module =Module.load(getTestFilePath(NON_PTE_FILE_NAME))
module.loadMethod(FORWARD_METHOD) // expected to throw
}

If loadMethod doesn't throw (test expectation fails), the module is never destroyed. Consider using assertThrows with a finally block for cleanup.

7. ReentrantLock callback re-entrancy in LlmModule

File:LlmModule.java:322-331

The generate() method holds the ReentrantLock while native code invokes LlmCallback methods. Since the lock is reentrant, callbacks running on the same thread could re-enter other LlmModule methods (e.g., resetContext(), close()). The close() has a getHoldCount() > 1 guard, but other methods don't. This is an inherent limitation of using ReentrantLock — documenting that callbacks must not call back into LlmModule (except stop()) would help.


What Looks Good

  • ExecutorchRuntimeException: The error code mapping with human-readable messages and hex codes is excellent for debugging. The makeExecutorchException factory pattern is clean.
  • ExecuTorchRuntime.validateFilePath: Clear, specific error messages for each failure case (null, doesn't exist, not a file, not readable). This addresses the earlier Copilot feedback about !! — the current implementation is already clean.
  • Module.close()/destroy() pattern: Using tryLock() to detect concurrent execution and throwing rather than blocking is the right call for lifecycle methods.
  • TrainingModule: Simple and correct — checkNotDestroyed() + Closeable with idempotent close().
  • AsrModule: Good use of AtomicLong + getAndSet(0L) for thread-safe idempotent close without needing a lock.
  • JNI error surfacing: Both jni_layer.cpp and jni_layer_llama.cpp now catch native exceptions during construction and convert them to Java exceptions — this was a significant gap before.
  • Test coverage: The multi-threaded forward test, destroyed-state tests for all public methods, and the LLM lifecycle tests provide good confidence.

Summary

The PR is in good shape overall. The 3 higher-priority items (#1 stop() guard, #2 std::move, #3 generate exception handling) are worth fixing before merge as they represent potential crashes or UB. The rest are polish items.


CopilotAI review requested due to automatic review settings May 12, 2026 21:39
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 4558d37 to 0415dd1CompareMay 12, 2026 21:46

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment threadextension/android/jni/jni_layer.cpp
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 0415dd1 to e72eecdCompareMay 12, 2026 21:55
psiddh added a commit to psiddh/executorch that referenced this pull request May 13, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
CopilotAI review requested due to automatic review settings May 13, 2026 06:04
@psiddh
psiddhforce-pushed the android-combined-v2 branch from e72eecd to 881a8d5CompareMay 13, 2026 06:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 881a8d5 to 85cc5e4CompareMay 13, 2026 07:56
@meta-codesync
meta-codesyncBot merged commit 3cc1ba9 into pytorch:mainMay 14, 2026
168 of 177 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@psiddh@kirklandsign
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229) - #19229

Merged
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2
May 14, 2026
Merged

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)#19229
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2

Conversation

@psiddh

@psiddhpsiddh commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Summary:
Combines all previously reverted Android improvement PRs (#18669, #19012, #19028, #19092, #19099, #19124) plus new Module lifecycle tests into a single atomic change.

Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.

Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.

Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.

JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).

Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.

This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.

Reviewed By: kirklandsign

CopilotAI review requested due to automatic review settings April 30, 2026 16:38
@pytorch-bot

pytorch-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/19229

Note: Links to docs will display an error until the docs builds have been completed.

❗ 1 Active SEVs

There are 1 currently active SEVs. If your PR is affected, please view them below:

❌ 2 New Failures, 4 Unrelated Failures

As of commit 85cc5e4 with merge base fe98297 (image):

NEW FAILURES - The following jobs have failed:

FLAKY - The following job failed but was likely due to flakiness present on trunk:

BROKEN TRUNK - The following jobs failed but was present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Apr 30, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Consolidates several Android API improvements across the ExecuTorch Java/Kotlin wrappers and JNI layer, focusing on exception-based error reporting, explicit lifecycle management (Closeable), and basic thread-safety guarantees, plus expanded instrumentation test coverage.

Changes:

  • Standardize synchronous error handling to throw exceptions (vs returning status codes / silent logging) across Module, LlmModule, training/ASR wrappers, and JNI glue.
  • Add/solidify lifecycle management via Closeable (close()/idempotent destroy patterns) and serialize access to non-thread-safe native state in LlmModule via ReentrantLock.
  • Update JNI registrations and Java native method naming (*Native), and expand Android instrumentation tests to cover lifecycle and API behavior.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaHandle Module.loadMethod() throwing by capturing an error code and early-returning safely; ensure destroy() in finally.
extension/android/jni/jni_layer_llama.cppAdd constructor exception-to-Java conversion; improve load() error reporting; rename registered natives (generateNative, loadNative, resetContextNative).
extension/android/jni/jni_layer.cppWrap native Module construction in exception mapping; throw on unsupported input EValue type codes; rename registered natives (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplement Closeable; replace silent failures with IllegalStateException on use-after-destroy.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaThrow IllegalStateException (vs generic RuntimeException) when optimizer is destroyed.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplement Closeable; add locking + destroyed checks; convert many status-returning APIs to void + exception; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktThrow ExecutorchRuntimeException with error code on creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplement Closeable; make loadMethod() throw; add locked wrappers around additional APIs; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdd error-code documentation, ALREADY_LOADED, improved message prefix, and cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaTighten file-path validation and switch to IllegalArgumentException.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignore and fix tests by providing required dummy input; add new API/lifecycle coverage.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdate tests for exception-based load; add close() lifecycle tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a96bd6b to 6ffdd62CompareApril 30, 2026 16:47
CopilotAI review requested due to automatic review settings April 30, 2026 17:05
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 787239e to 9f8afe1CompareApril 30, 2026 17:06
@psiddh
psiddh marked this pull request as draft April 30, 2026 17:10

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR consolidates several previously reverted Android improvements into one atomic update, standardizing ExecuTorch Android APIs around exception-based error reporting, explicit Closeable lifecycles, and improved thread-safety—along with significant new/updated instrumentation test coverage.

Changes:

  • Shift Android APIs away from status-code returns/silent failures to consistent exception throwing (Java/Kotlin + JNI).
  • Add/align Closeable lifecycle semantics (try-with-resources) and introduce locking around non-thread-safe native state (notably for LlmModule).
  • Update JNI registrations to match renamed *Native methods and expand Android instrumentation tests (including new lifecycle tests).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaAdapts benchmark load-path to exception-based Module.loadMethod() and ensures destroy on success path.
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.javaAdapts load-path to exception-based LlmModule.load() (but generate path still needs updates).
extension/android/jni/jni_layer_llama.cppAdds constructor error surfacing, improves load error reporting, and updates native method registrations (*Native).
extension/android/jni/jni_layer.cppAdds constructor error surfacing, throws on unsupported EValue input type, and updates native registrations (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplements Closeable and replaces silent failures with exceptions.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaUses IllegalStateException for destroyed optimizer usage.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplements Closeable, adds locking, converts sync APIs to throw-on-error wrappers, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktSwitches to ExecutorchRuntimeException for native creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplements Closeable, adds throw-on-error wrappers and lock-guarded public methods, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdds richer docs, new error code constant, improved message format, and a cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaMakes file-path validation stricter and consistently IllegalArgumentException-based.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignores/fixes Module tests and adds broader API/lifecycle coverage (load modes, log buffer, etdump, destroyed-state).
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdates to new throwing APIs and adds lifecycle tests (use-after-close, idempotent close).
Comments suppressed due to low confidence (1)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:102

  • LlmModule.generate(...) now throws on failure, but the MESSAGE_GENERATE branch doesn't catch exceptions. An exception here will crash the HandlerThread and can prevent onGenerationStopped() from running / results from being recorded. Wrap the generate call in try/catch (ideally extracting an error code from ExecutorchRuntimeException) and route the failure to the callback (e.g., call onGenerationStopped() and/or add an explicit error callback).
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has imported this pull request. If you are a Meta employee, you can view this in D103233465.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a44e428 to 04727f0CompareApril 30, 2026 18:19
@meta-codesyncmeta-codesyncBot changed the title Android: Closeable lifecycle, error reporting, thread safety, and tes…Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)May 5, 2026
psiddh added a commit to psiddh/executorch that referenced this pull request May 5, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 9adb92f to 274552bCompareMay 5, 2026 16:13
@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has exported this pull request. If you are a Meta employee, you can view the originating Diff in D103233465.

@psiddh
psiddh marked this pull request as ready for review May 5, 2026 16:18
CopilotAI review requested due to automatic review settings May 5, 2026 16:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:103

  • LlmModule.generate(...) now throws on failure (it no longer returns a status code). In the MESSAGE_GENERATE path, any exception will escape handleMessage, potentially killing the handler thread and skipping onGenerationStopped(). Wrap generate() in try/catch and ensure the callback is notified (and/or propagate an error status) instead of letting the background thread crash.
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.kt:62

  • These tests create a new LlmModule in setUp() for every test case but never close it, so native resources can accumulate across the suite (and potentially cause flakiness/OOM). Add an @After that calls llmModule.close() (or wrap each test body in try/finally) so the module is always released.
 llmModule =
LlmModule(getTestFilePath(TEST_FILE_NAME), getTestFilePath(TOKENIZER_FILE_NAME), 0.0f)
}
@Test
@Throws(IOException::class, URISyntaxException::class)
fun testGenerate() {
llmModule.load()
llmModule.generate(TEST_PROMPT, SEQ_LEN, this@LlmModuleInstrumentationTest)
assertEquals(results.size.toLong(), SEQ_LEN.toLong())
assertTrue(tokensPerSecond[tokensPerSecond.size - 1] > 0)
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/android/jni/jni_layer.cpp
@psiddh

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR and the comments

@claude

claudeBot commented May 8, 2026

Copy link
Copy Markdown

Claude finished @psiddh's task in 2m 14s —— View job


PR Review: Android Closeable lifecycle, error reporting, thread safety, and test coverage

  • Gather context and understand the request
  • Review core Java/Kotlin changes (Module, LlmModule, error handling)
  • Review JNI layer changes
  • Review benchmark/caller updates
  • Review test coverage
  • Assess existing review comments and provide consolidated feedback

Overall Assessment

This is a well-structured consolidation of several improvements. The move from status-code returns to exception-based error reporting is the right direction for Android APIs, and the Closeable lifecycle + ReentrantLock serialization are meaningful safety improvements. The test coverage additions are solid.

That said, there are a few issues worth addressing — some flagged by Copilot that I agree with, and some additional ones.


Issues I'd Recommend Fixing

1. stop() has no use-after-close guard (High Priority)

File:LlmModule.java:782

stop() is a bare public native void stop() with no mDestroyed check. Every other public method checks mDestroyed and throws IllegalStateException, but stop() would call into released native memory after close(). Since stop() intentionally skips the lock (it uses a C++ atomic flag), a simple volatile check is sufficient:

publicvoidstop() {
if (mDestroyed) thrownewIllegalStateException("LlmModule has been destroyed");
stopNative();
}
@DoNotStripprivatenativevoidstopNative();

This also requires updating the JNI registration in jni_layer_llama.cpp to map "stopNative"ExecuTorchLlmJni::stop.

Fix this →

2. Missing std::move for etdump_gen in profiling build (Medium Priority)

File:jni_layer.cpp:288-289

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, etdump_gen);

The Module constructor takes std::unique_ptr<runtime::EventTracer> by value. In the EXECUTORCH_ANDROID_PROFILING path, etdump_gen is a std::unique_ptr<ETDumpGen> — passing it without std::move won't compile. This should be:

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, std::move(etdump_gen));

The non-profiling path (auto etdump_gen = nullptr) works fine since nullptr implicitly converts to a null unique_ptr.

Fix this →

3. LlmModelRunner generate path lacks exception handling (Medium Priority)

File:LlmModelRunner.java:101-103

} elseif (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

generate() now throws ExecutorchRuntimeException on failure, but this path doesn't catch it. An exception here kills the HandlerThread and skips onGenerationStopped(). Should be wrapped like the load path:

} elseif (msg.what == MESSAGE_GENERATE) {
try {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
} catch (Exceptione) {
// Optionally surface error to callback
}
mLlmModelRunner.mCallback.onGenerationStopped();
}

Fix this →

4. LlmModuleInstrumentationTest leaks native resources (Low Priority)

File:LlmModuleInstrumentationTest.kt

setUp() creates a new LlmModule for each test but there's no @After method calling close(). Tests that don't explicitly close (e.g., testGenerate, testGenerateAndStop, all the prefill validation tests) leak native memory. Add:

@After
funtearDown() {
if (::llmModule.isInitialized) {
llmModule.close()
}
}

Minor Observations (Non-blocking)

5. testMethodMetadata() is a no-op test

File:ModuleInstrumentationTest.kt:56-59

funtestMethodMetadata() {
val module =Module.load(getTestFilePath(TEST_FILE_NAME))
module.destroy()
}

This test loads and immediately destroys without asserting anything about metadata. The newer testGetMethodMetadata() at line 216 does the actual work. The old test is dead code — consider removing it.

6. testNonPteFile doesn't clean up on unexpected success

File:ModuleInstrumentationTest.kt:112-117

funtestNonPteFile() {
val module =Module.load(getTestFilePath(NON_PTE_FILE_NAME))
module.loadMethod(FORWARD_METHOD) // expected to throw
}

If loadMethod doesn't throw (test expectation fails), the module is never destroyed. Consider using assertThrows with a finally block for cleanup.

7. ReentrantLock callback re-entrancy in LlmModule

File:LlmModule.java:322-331

The generate() method holds the ReentrantLock while native code invokes LlmCallback methods. Since the lock is reentrant, callbacks running on the same thread could re-enter other LlmModule methods (e.g., resetContext(), close()). The close() has a getHoldCount() > 1 guard, but other methods don't. This is an inherent limitation of using ReentrantLock — documenting that callbacks must not call back into LlmModule (except stop()) would help.


What Looks Good

  • ExecutorchRuntimeException: The error code mapping with human-readable messages and hex codes is excellent for debugging. The makeExecutorchException factory pattern is clean.
  • ExecuTorchRuntime.validateFilePath: Clear, specific error messages for each failure case (null, doesn't exist, not a file, not readable). This addresses the earlier Copilot feedback about !! — the current implementation is already clean.
  • Module.close()/destroy() pattern: Using tryLock() to detect concurrent execution and throwing rather than blocking is the right call for lifecycle methods.
  • TrainingModule: Simple and correct — checkNotDestroyed() + Closeable with idempotent close().
  • AsrModule: Good use of AtomicLong + getAndSet(0L) for thread-safe idempotent close without needing a lock.
  • JNI error surfacing: Both jni_layer.cpp and jni_layer_llama.cpp now catch native exceptions during construction and convert them to Java exceptions — this was a significant gap before.
  • Test coverage: The multi-threaded forward test, destroyed-state tests for all public methods, and the LLM lifecycle tests provide good confidence.

Summary

The PR is in good shape overall. The 3 higher-priority items (#1 stop() guard, #2 std::move, #3 generate exception handling) are worth fixing before merge as they represent potential crashes or UB. The rest are polish items.


CopilotAI review requested due to automatic review settings May 12, 2026 21:39
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 4558d37 to 0415dd1CompareMay 12, 2026 21:46

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment threadextension/android/jni/jni_layer.cpp
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 0415dd1 to e72eecdCompareMay 12, 2026 21:55
psiddh added a commit to psiddh/executorch that referenced this pull request May 13, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
CopilotAI review requested due to automatic review settings May 13, 2026 06:04
@psiddh
psiddhforce-pushed the android-combined-v2 branch from e72eecd to 881a8d5CompareMay 13, 2026 06:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 881a8d5 to 85cc5e4CompareMay 13, 2026 07:56
@meta-codesync
meta-codesyncBot merged commit 3cc1ba9 into pytorch:mainMay 14, 2026
168 of 177 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@psiddh@kirklandsign
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229) - #19229

Merged
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2
May 14, 2026
Merged

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)#19229
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2

Conversation

@psiddh

@psiddhpsiddh commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Summary:
Combines all previously reverted Android improvement PRs (#18669, #19012, #19028, #19092, #19099, #19124) plus new Module lifecycle tests into a single atomic change.

Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.

Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.

Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.

JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).

Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.

This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.

Reviewed By: kirklandsign

CopilotAI review requested due to automatic review settings April 30, 2026 16:38
@pytorch-bot

pytorch-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/19229

Note: Links to docs will display an error until the docs builds have been completed.

❗ 1 Active SEVs

There are 1 currently active SEVs. If your PR is affected, please view them below:

❌ 2 New Failures, 4 Unrelated Failures

As of commit 85cc5e4 with merge base fe98297 (image):

NEW FAILURES - The following jobs have failed:

FLAKY - The following job failed but was likely due to flakiness present on trunk:

BROKEN TRUNK - The following jobs failed but was present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Apr 30, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Consolidates several Android API improvements across the ExecuTorch Java/Kotlin wrappers and JNI layer, focusing on exception-based error reporting, explicit lifecycle management (Closeable), and basic thread-safety guarantees, plus expanded instrumentation test coverage.

Changes:

  • Standardize synchronous error handling to throw exceptions (vs returning status codes / silent logging) across Module, LlmModule, training/ASR wrappers, and JNI glue.
  • Add/solidify lifecycle management via Closeable (close()/idempotent destroy patterns) and serialize access to non-thread-safe native state in LlmModule via ReentrantLock.
  • Update JNI registrations and Java native method naming (*Native), and expand Android instrumentation tests to cover lifecycle and API behavior.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaHandle Module.loadMethod() throwing by capturing an error code and early-returning safely; ensure destroy() in finally.
extension/android/jni/jni_layer_llama.cppAdd constructor exception-to-Java conversion; improve load() error reporting; rename registered natives (generateNative, loadNative, resetContextNative).
extension/android/jni/jni_layer.cppWrap native Module construction in exception mapping; throw on unsupported input EValue type codes; rename registered natives (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplement Closeable; replace silent failures with IllegalStateException on use-after-destroy.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaThrow IllegalStateException (vs generic RuntimeException) when optimizer is destroyed.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplement Closeable; add locking + destroyed checks; convert many status-returning APIs to void + exception; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktThrow ExecutorchRuntimeException with error code on creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplement Closeable; make loadMethod() throw; add locked wrappers around additional APIs; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdd error-code documentation, ALREADY_LOADED, improved message prefix, and cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaTighten file-path validation and switch to IllegalArgumentException.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignore and fix tests by providing required dummy input; add new API/lifecycle coverage.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdate tests for exception-based load; add close() lifecycle tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a96bd6b to 6ffdd62CompareApril 30, 2026 16:47
CopilotAI review requested due to automatic review settings April 30, 2026 17:05
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 787239e to 9f8afe1CompareApril 30, 2026 17:06
@psiddh
psiddh marked this pull request as draft April 30, 2026 17:10

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR consolidates several previously reverted Android improvements into one atomic update, standardizing ExecuTorch Android APIs around exception-based error reporting, explicit Closeable lifecycles, and improved thread-safety—along with significant new/updated instrumentation test coverage.

Changes:

  • Shift Android APIs away from status-code returns/silent failures to consistent exception throwing (Java/Kotlin + JNI).
  • Add/align Closeable lifecycle semantics (try-with-resources) and introduce locking around non-thread-safe native state (notably for LlmModule).
  • Update JNI registrations to match renamed *Native methods and expand Android instrumentation tests (including new lifecycle tests).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaAdapts benchmark load-path to exception-based Module.loadMethod() and ensures destroy on success path.
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.javaAdapts load-path to exception-based LlmModule.load() (but generate path still needs updates).
extension/android/jni/jni_layer_llama.cppAdds constructor error surfacing, improves load error reporting, and updates native method registrations (*Native).
extension/android/jni/jni_layer.cppAdds constructor error surfacing, throws on unsupported EValue input type, and updates native registrations (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplements Closeable and replaces silent failures with exceptions.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaUses IllegalStateException for destroyed optimizer usage.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplements Closeable, adds locking, converts sync APIs to throw-on-error wrappers, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktSwitches to ExecutorchRuntimeException for native creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplements Closeable, adds throw-on-error wrappers and lock-guarded public methods, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdds richer docs, new error code constant, improved message format, and a cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaMakes file-path validation stricter and consistently IllegalArgumentException-based.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignores/fixes Module tests and adds broader API/lifecycle coverage (load modes, log buffer, etdump, destroyed-state).
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdates to new throwing APIs and adds lifecycle tests (use-after-close, idempotent close).
Comments suppressed due to low confidence (1)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:102

  • LlmModule.generate(...) now throws on failure, but the MESSAGE_GENERATE branch doesn't catch exceptions. An exception here will crash the HandlerThread and can prevent onGenerationStopped() from running / results from being recorded. Wrap the generate call in try/catch (ideally extracting an error code from ExecutorchRuntimeException) and route the failure to the callback (e.g., call onGenerationStopped() and/or add an explicit error callback).
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has imported this pull request. If you are a Meta employee, you can view this in D103233465.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a44e428 to 04727f0CompareApril 30, 2026 18:19
@meta-codesyncmeta-codesyncBot changed the title Android: Closeable lifecycle, error reporting, thread safety, and tes…Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)May 5, 2026
psiddh added a commit to psiddh/executorch that referenced this pull request May 5, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 9adb92f to 274552bCompareMay 5, 2026 16:13
@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has exported this pull request. If you are a Meta employee, you can view the originating Diff in D103233465.

@psiddh
psiddh marked this pull request as ready for review May 5, 2026 16:18
CopilotAI review requested due to automatic review settings May 5, 2026 16:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:103

  • LlmModule.generate(...) now throws on failure (it no longer returns a status code). In the MESSAGE_GENERATE path, any exception will escape handleMessage, potentially killing the handler thread and skipping onGenerationStopped(). Wrap generate() in try/catch and ensure the callback is notified (and/or propagate an error status) instead of letting the background thread crash.
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.kt:62

  • These tests create a new LlmModule in setUp() for every test case but never close it, so native resources can accumulate across the suite (and potentially cause flakiness/OOM). Add an @After that calls llmModule.close() (or wrap each test body in try/finally) so the module is always released.
 llmModule =
LlmModule(getTestFilePath(TEST_FILE_NAME), getTestFilePath(TOKENIZER_FILE_NAME), 0.0f)
}
@Test
@Throws(IOException::class, URISyntaxException::class)
fun testGenerate() {
llmModule.load()
llmModule.generate(TEST_PROMPT, SEQ_LEN, this@LlmModuleInstrumentationTest)
assertEquals(results.size.toLong(), SEQ_LEN.toLong())
assertTrue(tokensPerSecond[tokensPerSecond.size - 1] > 0)
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/android/jni/jni_layer.cpp
@psiddh

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR and the comments

@claude

claudeBot commented May 8, 2026

Copy link
Copy Markdown

Claude finished @psiddh's task in 2m 14s —— View job


PR Review: Android Closeable lifecycle, error reporting, thread safety, and test coverage

  • Gather context and understand the request
  • Review core Java/Kotlin changes (Module, LlmModule, error handling)
  • Review JNI layer changes
  • Review benchmark/caller updates
  • Review test coverage
  • Assess existing review comments and provide consolidated feedback

Overall Assessment

This is a well-structured consolidation of several improvements. The move from status-code returns to exception-based error reporting is the right direction for Android APIs, and the Closeable lifecycle + ReentrantLock serialization are meaningful safety improvements. The test coverage additions are solid.

That said, there are a few issues worth addressing — some flagged by Copilot that I agree with, and some additional ones.


Issues I'd Recommend Fixing

1. stop() has no use-after-close guard (High Priority)

File:LlmModule.java:782

stop() is a bare public native void stop() with no mDestroyed check. Every other public method checks mDestroyed and throws IllegalStateException, but stop() would call into released native memory after close(). Since stop() intentionally skips the lock (it uses a C++ atomic flag), a simple volatile check is sufficient:

publicvoidstop() {
if (mDestroyed) thrownewIllegalStateException("LlmModule has been destroyed");
stopNative();
}
@DoNotStripprivatenativevoidstopNative();

This also requires updating the JNI registration in jni_layer_llama.cpp to map "stopNative"ExecuTorchLlmJni::stop.

Fix this →

2. Missing std::move for etdump_gen in profiling build (Medium Priority)

File:jni_layer.cpp:288-289

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, etdump_gen);

The Module constructor takes std::unique_ptr<runtime::EventTracer> by value. In the EXECUTORCH_ANDROID_PROFILING path, etdump_gen is a std::unique_ptr<ETDumpGen> — passing it without std::move won't compile. This should be:

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, std::move(etdump_gen));

The non-profiling path (auto etdump_gen = nullptr) works fine since nullptr implicitly converts to a null unique_ptr.

Fix this →

3. LlmModelRunner generate path lacks exception handling (Medium Priority)

File:LlmModelRunner.java:101-103

} elseif (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

generate() now throws ExecutorchRuntimeException on failure, but this path doesn't catch it. An exception here kills the HandlerThread and skips onGenerationStopped(). Should be wrapped like the load path:

} elseif (msg.what == MESSAGE_GENERATE) {
try {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
} catch (Exceptione) {
// Optionally surface error to callback
}
mLlmModelRunner.mCallback.onGenerationStopped();
}

Fix this →

4. LlmModuleInstrumentationTest leaks native resources (Low Priority)

File:LlmModuleInstrumentationTest.kt

setUp() creates a new LlmModule for each test but there's no @After method calling close(). Tests that don't explicitly close (e.g., testGenerate, testGenerateAndStop, all the prefill validation tests) leak native memory. Add:

@After
funtearDown() {
if (::llmModule.isInitialized) {
llmModule.close()
}
}

Minor Observations (Non-blocking)

5. testMethodMetadata() is a no-op test

File:ModuleInstrumentationTest.kt:56-59

funtestMethodMetadata() {
val module =Module.load(getTestFilePath(TEST_FILE_NAME))
module.destroy()
}

This test loads and immediately destroys without asserting anything about metadata. The newer testGetMethodMetadata() at line 216 does the actual work. The old test is dead code — consider removing it.

6. testNonPteFile doesn't clean up on unexpected success

File:ModuleInstrumentationTest.kt:112-117

funtestNonPteFile() {
val module =Module.load(getTestFilePath(NON_PTE_FILE_NAME))
module.loadMethod(FORWARD_METHOD) // expected to throw
}

If loadMethod doesn't throw (test expectation fails), the module is never destroyed. Consider using assertThrows with a finally block for cleanup.

7. ReentrantLock callback re-entrancy in LlmModule

File:LlmModule.java:322-331

The generate() method holds the ReentrantLock while native code invokes LlmCallback methods. Since the lock is reentrant, callbacks running on the same thread could re-enter other LlmModule methods (e.g., resetContext(), close()). The close() has a getHoldCount() > 1 guard, but other methods don't. This is an inherent limitation of using ReentrantLock — documenting that callbacks must not call back into LlmModule (except stop()) would help.


What Looks Good

  • ExecutorchRuntimeException: The error code mapping with human-readable messages and hex codes is excellent for debugging. The makeExecutorchException factory pattern is clean.
  • ExecuTorchRuntime.validateFilePath: Clear, specific error messages for each failure case (null, doesn't exist, not a file, not readable). This addresses the earlier Copilot feedback about !! — the current implementation is already clean.
  • Module.close()/destroy() pattern: Using tryLock() to detect concurrent execution and throwing rather than blocking is the right call for lifecycle methods.
  • TrainingModule: Simple and correct — checkNotDestroyed() + Closeable with idempotent close().
  • AsrModule: Good use of AtomicLong + getAndSet(0L) for thread-safe idempotent close without needing a lock.
  • JNI error surfacing: Both jni_layer.cpp and jni_layer_llama.cpp now catch native exceptions during construction and convert them to Java exceptions — this was a significant gap before.
  • Test coverage: The multi-threaded forward test, destroyed-state tests for all public methods, and the LLM lifecycle tests provide good confidence.

Summary

The PR is in good shape overall. The 3 higher-priority items (#1 stop() guard, #2 std::move, #3 generate exception handling) are worth fixing before merge as they represent potential crashes or UB. The rest are polish items.


CopilotAI review requested due to automatic review settings May 12, 2026 21:39
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 4558d37 to 0415dd1CompareMay 12, 2026 21:46

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment threadextension/android/jni/jni_layer.cpp
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 0415dd1 to e72eecdCompareMay 12, 2026 21:55
psiddh added a commit to psiddh/executorch that referenced this pull request May 13, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
CopilotAI review requested due to automatic review settings May 13, 2026 06:04
@psiddh
psiddhforce-pushed the android-combined-v2 branch from e72eecd to 881a8d5CompareMay 13, 2026 06:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 881a8d5 to 85cc5e4CompareMay 13, 2026 07:56
@meta-codesync
meta-codesyncBot merged commit 3cc1ba9 into pytorch:mainMay 14, 2026
168 of 177 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@psiddh@kirklandsign
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229) - #19229

Merged
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2
May 14, 2026
Merged

Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)#19229
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
psiddh:android-combined-v2

Conversation

@psiddh

@psiddhpsiddh commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Summary:
Combines all previously reverted Android improvement PRs (#18669, #19012, #19028, #19092, #19099, #19124) plus new Module lifecycle tests into a single atomic change.

Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.

Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.

Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.

JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).

Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.

This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.

Reviewed By: kirklandsign

CopilotAI review requested due to automatic review settings April 30, 2026 16:38
@pytorch-bot

pytorch-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/19229

Note: Links to docs will display an error until the docs builds have been completed.

❗ 1 Active SEVs

There are 1 currently active SEVs. If your PR is affected, please view them below:

❌ 2 New Failures, 4 Unrelated Failures

As of commit 85cc5e4 with merge base fe98297 (image):

NEW FAILURES - The following jobs have failed:

FLAKY - The following job failed but was likely due to flakiness present on trunk:

BROKEN TRUNK - The following jobs failed but was present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Apr 30, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Consolidates several Android API improvements across the ExecuTorch Java/Kotlin wrappers and JNI layer, focusing on exception-based error reporting, explicit lifecycle management (Closeable), and basic thread-safety guarantees, plus expanded instrumentation test coverage.

Changes:

  • Standardize synchronous error handling to throw exceptions (vs returning status codes / silent logging) across Module, LlmModule, training/ASR wrappers, and JNI glue.
  • Add/solidify lifecycle management via Closeable (close()/idempotent destroy patterns) and serialize access to non-thread-safe native state in LlmModule via ReentrantLock.
  • Update JNI registrations and Java native method naming (*Native), and expand Android instrumentation tests to cover lifecycle and API behavior.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaHandle Module.loadMethod() throwing by capturing an error code and early-returning safely; ensure destroy() in finally.
extension/android/jni/jni_layer_llama.cppAdd constructor exception-to-Java conversion; improve load() error reporting; rename registered natives (generateNative, loadNative, resetContextNative).
extension/android/jni/jni_layer.cppWrap native Module construction in exception mapping; throw on unsupported input EValue type codes; rename registered natives (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplement Closeable; replace silent failures with IllegalStateException on use-after-destroy.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaThrow IllegalStateException (vs generic RuntimeException) when optimizer is destroyed.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplement Closeable; add locking + destroyed checks; convert many status-returning APIs to void + exception; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktThrow ExecutorchRuntimeException with error code on creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplement Closeable; make loadMethod() throw; add locked wrappers around additional APIs; rename native methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdd error-code documentation, ALREADY_LOADED, improved message prefix, and cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaTighten file-path validation and switch to IllegalArgumentException.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignore and fix tests by providing required dummy input; add new API/lifecycle coverage.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdate tests for exception-based load; add close() lifecycle tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a96bd6b to 6ffdd62CompareApril 30, 2026 16:47
CopilotAI review requested due to automatic review settings April 30, 2026 17:05
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 787239e to 9f8afe1CompareApril 30, 2026 17:06
@psiddh
psiddh marked this pull request as draft April 30, 2026 17:10

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR consolidates several previously reverted Android improvements into one atomic update, standardizing ExecuTorch Android APIs around exception-based error reporting, explicit Closeable lifecycles, and improved thread-safety—along with significant new/updated instrumentation test coverage.

Changes:

  • Shift Android APIs away from status-code returns/silent failures to consistent exception throwing (Java/Kotlin + JNI).
  • Add/align Closeable lifecycle semantics (try-with-resources) and introduce locking around non-thread-safe native state (notably for LlmModule).
  • Update JNI registrations to match renamed *Native methods and expand Android instrumentation tests (including new lifecycle tests).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/ModelRunner.javaAdapts benchmark load-path to exception-based Module.loadMethod() and ensures destroy on success path.
extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.javaAdapts load-path to exception-based LlmModule.load() (but generate path still needs updates).
extension/android/jni/jni_layer_llama.cppAdds constructor error surfacing, improves load error reporting, and updates native method registrations (*Native).
extension/android/jni/jni_layer.cppAdds constructor error surfacing, throws on unsupported EValue input type, and updates native registrations (etdumpNative, getMethodsNative).
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/TrainingModule.javaImplements Closeable and replaces silent failures with exceptions.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/training/SGD.javaUses IllegalStateException for destroyed optimizer usage.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/llm/LlmModule.javaImplements Closeable, adds locking, converts sync APIs to throw-on-error wrappers, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/extension/asr/AsrModule.ktSwitches to ExecutorchRuntimeException for native creation/transcription failures.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/Module.javaImplements Closeable, adds throw-on-error wrappers and lock-guarded public methods, and renames JNI methods to *Native.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecutorchRuntimeException.javaAdds richer docs, new error code constant, improved message format, and a cause-chaining constructor.
extension/android/executorch_android/src/main/java/org/pytorch/executorch/ExecuTorchRuntime.javaMakes file-path validation stricter and consistently IllegalArgumentException-based.
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/ModuleInstrumentationTest.ktUn-ignores/fixes Module tests and adds broader API/lifecycle coverage (load modes, log buffer, etdump, destroyed-state).
extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.ktUpdates to new throwing APIs and adds lifecycle tests (use-after-close, idempotent close).
Comments suppressed due to low confidence (1)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:102

  • LlmModule.generate(...) now throws on failure, but the MESSAGE_GENERATE branch doesn't catch exceptions. An exception here will crash the HandlerThread and can prevent onGenerationStopped() from running / results from being recorded. Wrap the generate call in try/catch (ideally extracting an error code from ExecutorchRuntimeException) and route the failure to the callback (e.g., call onGenerationStopped() and/or add an explicit error callback).
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has imported this pull request. If you are a Meta employee, you can view this in D103233465.

@psiddh
psiddhforce-pushed the android-combined-v2 branch from a44e428 to 04727f0CompareApril 30, 2026 18:19
@meta-codesyncmeta-codesyncBot changed the title Android: Closeable lifecycle, error reporting, thread safety, and tes…Android: Closeable lifecycle, error reporting, thread safety, and test coverage (#19229)May 5, 2026
psiddh added a commit to psiddh/executorch that referenced this pull request May 5, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 9adb92f to 274552bCompareMay 5, 2026 16:13
@meta-codesync

Copy link
Copy Markdown
Contributor

@psiddh has exported this pull request. If you are a Meta employee, you can view the originating Diff in D103233465.

@psiddh
psiddh marked this pull request as ready for review May 5, 2026 16:18
CopilotAI review requested due to automatic review settings May 5, 2026 16:18

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

extension/benchmark/android/benchmark/app/src/main/java/org/pytorch/minibench/LlmModelRunner.java:103

  • LlmModule.generate(...) now throws on failure (it no longer returns a status code). In the MESSAGE_GENERATE path, any exception will escape handleMessage, potentially killing the handler thread and skipping onGenerationStopped(). Wrap generate() in try/catch and ensure the callback is notified (and/or propagate an error status) instead of letting the background thread crash.
 mLlmModelRunner.mCallback.onModelLoaded(status);
} else if (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

extension/android/executorch_android/src/androidTest/java/org/pytorch/executorch/LlmModuleInstrumentationTest.kt:62

  • These tests create a new LlmModule in setUp() for every test case but never close it, so native resources can accumulate across the suite (and potentially cause flakiness/OOM). Add an @After that calls llmModule.close() (or wrap each test body in try/finally) so the module is always released.
 llmModule =
LlmModule(getTestFilePath(TEST_FILE_NAME), getTestFilePath(TOKENIZER_FILE_NAME), 0.0f)
}
@Test
@Throws(IOException::class, URISyntaxException::class)
fun testGenerate() {
llmModule.load()
llmModule.generate(TEST_PROMPT, SEQ_LEN, this@LlmModuleInstrumentationTest)
assertEquals(results.size.toLong(), SEQ_LEN.toLong())
assertTrue(tokensPerSecond[tokensPerSecond.size - 1] > 0)
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextension/android/jni/jni_layer.cpp
@psiddh

Copy link
Copy Markdown
ContributorAuthor

@claude review this PR and the comments

@claude

claudeBot commented May 8, 2026

Copy link
Copy Markdown

Claude finished @psiddh's task in 2m 14s —— View job


PR Review: Android Closeable lifecycle, error reporting, thread safety, and test coverage

  • Gather context and understand the request
  • Review core Java/Kotlin changes (Module, LlmModule, error handling)
  • Review JNI layer changes
  • Review benchmark/caller updates
  • Review test coverage
  • Assess existing review comments and provide consolidated feedback

Overall Assessment

This is a well-structured consolidation of several improvements. The move from status-code returns to exception-based error reporting is the right direction for Android APIs, and the Closeable lifecycle + ReentrantLock serialization are meaningful safety improvements. The test coverage additions are solid.

That said, there are a few issues worth addressing — some flagged by Copilot that I agree with, and some additional ones.


Issues I'd Recommend Fixing

1. stop() has no use-after-close guard (High Priority)

File:LlmModule.java:782

stop() is a bare public native void stop() with no mDestroyed check. Every other public method checks mDestroyed and throws IllegalStateException, but stop() would call into released native memory after close(). Since stop() intentionally skips the lock (it uses a C++ atomic flag), a simple volatile check is sufficient:

publicvoidstop() {
if (mDestroyed) thrownewIllegalStateException("LlmModule has been destroyed");
stopNative();
}
@DoNotStripprivatenativevoidstopNative();

This also requires updating the JNI registration in jni_layer_llama.cpp to map "stopNative"ExecuTorchLlmJni::stop.

Fix this →

2. Missing std::move for etdump_gen in profiling build (Medium Priority)

File:jni_layer.cpp:288-289

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, etdump_gen);

The Module constructor takes std::unique_ptr<runtime::EventTracer> by value. In the EXECUTORCH_ANDROID_PROFILING path, etdump_gen is a std::unique_ptr<ETDumpGen> — passing it without std::move won't compile. This should be:

module_ = std::make_unique<Module>(
modelPath->toStdString(), load_mode, std::move(etdump_gen));

The non-profiling path (auto etdump_gen = nullptr) works fine since nullptr implicitly converts to a null unique_ptr.

Fix this →

3. LlmModelRunner generate path lacks exception handling (Medium Priority)

File:LlmModelRunner.java:101-103

} elseif (msg.what == MESSAGE_GENERATE) {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
mLlmModelRunner.mCallback.onGenerationStopped();
}

generate() now throws ExecutorchRuntimeException on failure, but this path doesn't catch it. An exception here kills the HandlerThread and skips onGenerationStopped(). Should be wrapped like the load path:

} elseif (msg.what == MESSAGE_GENERATE) {
try {
mLlmModelRunner.mModule.generate((String) msg.obj, mLlmModelRunner);
} catch (Exceptione) {
// Optionally surface error to callback
}
mLlmModelRunner.mCallback.onGenerationStopped();
}

Fix this →

4. LlmModuleInstrumentationTest leaks native resources (Low Priority)

File:LlmModuleInstrumentationTest.kt

setUp() creates a new LlmModule for each test but there's no @After method calling close(). Tests that don't explicitly close (e.g., testGenerate, testGenerateAndStop, all the prefill validation tests) leak native memory. Add:

@After
funtearDown() {
if (::llmModule.isInitialized) {
llmModule.close()
}
}

Minor Observations (Non-blocking)

5. testMethodMetadata() is a no-op test

File:ModuleInstrumentationTest.kt:56-59

funtestMethodMetadata() {
val module =Module.load(getTestFilePath(TEST_FILE_NAME))
module.destroy()
}

This test loads and immediately destroys without asserting anything about metadata. The newer testGetMethodMetadata() at line 216 does the actual work. The old test is dead code — consider removing it.

6. testNonPteFile doesn't clean up on unexpected success

File:ModuleInstrumentationTest.kt:112-117

funtestNonPteFile() {
val module =Module.load(getTestFilePath(NON_PTE_FILE_NAME))
module.loadMethod(FORWARD_METHOD) // expected to throw
}

If loadMethod doesn't throw (test expectation fails), the module is never destroyed. Consider using assertThrows with a finally block for cleanup.

7. ReentrantLock callback re-entrancy in LlmModule

File:LlmModule.java:322-331

The generate() method holds the ReentrantLock while native code invokes LlmCallback methods. Since the lock is reentrant, callbacks running on the same thread could re-enter other LlmModule methods (e.g., resetContext(), close()). The close() has a getHoldCount() > 1 guard, but other methods don't. This is an inherent limitation of using ReentrantLock — documenting that callbacks must not call back into LlmModule (except stop()) would help.


What Looks Good

  • ExecutorchRuntimeException: The error code mapping with human-readable messages and hex codes is excellent for debugging. The makeExecutorchException factory pattern is clean.
  • ExecuTorchRuntime.validateFilePath: Clear, specific error messages for each failure case (null, doesn't exist, not a file, not readable). This addresses the earlier Copilot feedback about !! — the current implementation is already clean.
  • Module.close()/destroy() pattern: Using tryLock() to detect concurrent execution and throwing rather than blocking is the right call for lifecycle methods.
  • TrainingModule: Simple and correct — checkNotDestroyed() + Closeable with idempotent close().
  • AsrModule: Good use of AtomicLong + getAndSet(0L) for thread-safe idempotent close without needing a lock.
  • JNI error surfacing: Both jni_layer.cpp and jni_layer_llama.cpp now catch native exceptions during construction and convert them to Java exceptions — this was a significant gap before.
  • Test coverage: The multi-threaded forward test, destroyed-state tests for all public methods, and the LLM lifecycle tests provide good confidence.

Summary

The PR is in good shape overall. The 3 higher-priority items (#1 stop() guard, #2 std::move, #3 generate exception handling) are worth fixing before merge as they represent potential crashes or UB. The rest are polish items.


CopilotAI review requested due to automatic review settings May 12, 2026 21:39
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag). TrainingModule replaces Log.e silent failures with IllegalStateException.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods; new DoNotStrip added to renamed private native methods (generateNative, resetContextNative, loadNative).
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 4558d37 to 0415dd1CompareMay 12, 2026 21:46

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment threadextension/android/jni/jni_layer.cpp
psiddh added a commit to psiddh/executorch that referenced this pull request May 12, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 0415dd1 to e72eecdCompareMay 12, 2026 21:55
psiddh added a commit to psiddh/executorch that referenced this pull request May 13, 2026
…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
CopilotAI review requested due to automatic review settings May 13, 2026 06:04
@psiddh
psiddhforce-pushed the android-combined-v2 branch from e72eecd to 881a8d5CompareMay 13, 2026 06:04

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

…t coverage (pytorch#19229)
Summary:
Combines all previously reverted Android improvement PRs (pytorch#18669, pytorch#19012, pytorch#19028, pytorch#19092, pytorch#19099, pytorch#19124) plus new Module lifecycle tests into a single atomic change.
Error reporting: all sync errors throw exceptions instead of returning status codes or logging silently. Module.loadMethod() throws ExecutorchRuntimeException on failure. LlmModule.generate() and load() throw on error. Native methods renamed with "Native" suffix; public wrappers check status and throw.
Lifecycle: Module, LlmModule, and TrainingModule implement Closeable for try-with-resources. LlmModule adds ReentrantLock to serialize access to non-thread-safe native state. stop() remains lock-free (C++ atomic flag) with volatile mDestroyed guard to prevent use-after-close. TrainingModule replaces Log.e silent failures with IllegalStateException.
Thread safety: LlmModule adds checkNotReentrant() guard to all lock-acquiring public methods, preventing callbacks from re-entering module methods and corrupting native state. TrainingModule.mDestroyed made volatile for cross-thread visibility.
Error consistency: ExecuTorchRuntime.validateFilePath throws IllegalArgumentException. SGD throws IllegalStateException. AsrModule throws ExecutorchRuntimeException. Cause-chaining constructor added to ExecutorchRuntimeException.
JNI safety: Module and LlmModule constructors wrapped in try-catch to surface native initialization failures. LlmModule.load() uses throwExecutorchException with diagnostic detail. All DoNotStrip annotations preserved on JNI-called methods. std::move added for etdump_gen unique_ptr in profiling build path. LlmModelRunner generate path wrapped in try-catch to prevent HandlerThread death.
Test Plan:
fix 4 Ignored Module tests by providing required input tensor. Add 13 new lifecycle/API coverage tests. Add LlmModule use-after-close and idempotent close tests. Add @after tearDown to LlmModuleInstrumentationTest to prevent native resource leaks. Remove dead testMethodMetadata test. Fix testNonPteFile cleanup with assertThrows + finally.
This diff updates callers of Module.loadMethod() to use try/catch instead of checking the return code, calling destroy() on failure.
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed By: kirklandsign
Differential Revision: D103233465
Pulled By: psiddh
@psiddh
psiddhforce-pushed the android-combined-v2 branch from 881a8d5 to 85cc5e4CompareMay 13, 2026 07:56
@meta-codesync
meta-codesyncBot merged commit 3cc1ba9 into pytorch:mainMay 14, 2026
168 of 177 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@psiddh@kirklandsign