Skip to content

Fix uninitialised _outputNames in ETCoreMLModelProfiler causing crash when debug_buffer_size=0#18763

Merged
metascroy merged 1 commit intopytorch:mainfrom
notaJiminLee:fix/coreml-profiler-outputnames
Apr 10, 2026
Merged

Fix uninitialised _outputNames in ETCoreMLModelProfiler causing crash when debug_buffer_size=0#18763
metascroy merged 1 commit intopytorch:mainfrom
notaJiminLee:fix/coreml-profiler-outputnames

Conversation

@notaJiminLee
Copy link
Copy Markdown
Contributor

@notaJiminLee notaJiminLee commented Apr 8, 2026

Summary

ETCoreMLModelProfiler::initWithModel:configuration:error: does not initialise the _outputNames property, leaving it as nil. This causes an NSRangeException crash when enable_etdump=True with
debug_buffer_size=0.

Root cause:

When _outputNames is nil, set_model_outputs() iterates over nil.count (0) and produces an empty NSMutableArray. This empty array is returned as modelOutputs from profileModelWithInputs. Later,
ETCoreMLModelManager attempts to access modelOutputs[0].shape for dynamic shape resizing, triggering:

NSRangeException: *** -[__NSArrayM objectAtIndexedSubscript:]: index 0 beyond bounds for empty array

This manifests as RuntimeError: Caught an unknown exception! at the Python level.

Why this was not caught earlier:

When debug_buffer_size > 0, kIntermediateOutputs debug level is set, which enables both log_profiling_info and log_intermediate_tensors. In ETCoreMLModelAnalyzer::executeModelWithInputs, the
debugModelWithInputs path runs after profiling and overwrites the empty array with valid outputs, masking the bug.

Fix:

Add _outputNames = model.orderedOutputNames; in the initialiser, consistent with how outputNames is used in profilingInfoForOperationsAtPaths: at line 319.

Test plan

Tested on macOS 15.6 (arm64) with a CoreML model (input: 1x3x640x640, 3 outputs):

from executorch.runtime import Runtime, Verification
import torch

rt = Runtime.get()
prog = rt.load_program("output_coreml.pte",
                        verification=Verification.Minimal,
                        enable_etdump=True,
                        debug_buffer_size=0)
method = prog.load_method("forward")
out = method.execute(torch.randn(1, 3, 640, 640))  # Previously crashed
prog.write_etdump_result_to_file("profile.etdump", "debug.bin")

┌──────────────────────────────────┬────────────────────────┬──────────────────────────────────┐
│             ScenarioBeforeAfter               │
├──────────────────────────────────┼────────────────────────┼──────────────────────────────────┤
│ debug_buffer_size=0NSRangeException crashSuccess, ETDump with timing data │
├──────────────────────────────────┼────────────────────────┼──────────────────────────────────┤
│ debug_buffer_size>0 (sufficient) │ Success (bug masked)   │ Success                          │
├──────────────────────────────────┼────────────────────────┼──────────────────────────────────┤
│                                  │                        │                                  │
└──────────────────────────────────┴────────────────────────┴──────────────────────────────────┘

cc @kimishpatel @YifanShenSZ @cymbalrush @metascroy

The _outputNames property was not initialised in initWithModel:configuration:error:,
causing set_model_outputs() to produce an empty array when debug_buffer_size=0.
This led to an NSRangeException (index 0 beyond bounds for empty array) in
ETCoreMLModelManager when accessing modelOutputs[0].shape for dynamic shape resizing.

When debug_buffer_size > 0, the bug was masked because the debugging path
(debugModelWithInputs) overwrote the empty profiling output with valid results.
@pytorch-bot
Copy link
Copy Markdown

pytorch-bot Bot commented Apr 8, 2026

🔗 Helpful Links

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

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:

❌ 1 Awaiting Approval, 18 Cancelled Jobs, 2 Unrelated Failures

As of commit e30a7c1 with merge base 4afd7f9 (image):

AWAITING APPROVAL - The following workflow needs approval before CI can run:

CANCELLED JOBS - The following jobs were cancelled. Please retry:

BROKEN TRUNK - The following jobs failed but were 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-cla
Copy link
Copy Markdown

meta-cla Bot commented Apr 8, 2026

Hi @notaJiminLee!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@pytorch-bot
Copy link
Copy Markdown

pytorch-bot Bot commented Apr 8, 2026

Didn't find following labels among repository labels: release notes: bug fix

@notaJiminLee
Copy link
Copy Markdown
Contributor Author

@pytorchbot label "release notes: apple"

@pytorch-bot pytorch-bot Bot added the release notes: apple Changes to the Apple backend delegate label Apr 8, 2026
@meta-cla
Copy link
Copy Markdown

meta-cla Bot commented Apr 8, 2026

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@meta-cla meta-cla Bot 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 8, 2026
@nil-is-all nil-is-all added the module: coreml Issues related to Apple's Core ML delegation and code under backends/apple/coreml/ label Apr 8, 2026
@pytorch-bot
Copy link
Copy Markdown

pytorch-bot Bot commented Apr 10, 2026

To add the ciflow label ciflow/trunk please first approve the workflows that are awaiting approval (scroll to the bottom of this page).

This helps ensure we don't trigger CI on this PR until it is actually authorized to do so. Please ping one of the reviewers if you do not have access to approve and run workflows.

@meta-codesync
Copy link
Copy Markdown
Contributor

meta-codesync Bot commented Apr 10, 2026

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

@metascroy
Copy link
Copy Markdown
Contributor

@claude review this PR

@metascroy
Copy link
Copy Markdown
Contributor

Thank you for the fix @notaJiminLee! Let's see what CI says

@claude
Copy link
Copy Markdown

claude Bot commented Apr 10, 2026

Claude finished @metascroy's task in 2m 4s —— View job


Review of PR #18763

  • Read the changed file and understand the fix in context
  • Check the initialization pattern in related classes (e.g., ETCoreMLModelAnalyzer, ETCoreMLModelDebugger)
  • Verify usage of _outputNames in the profiler
  • Post review feedback

Verdict: This fix is correct and well-targeted.

The change adds a single missing initialization on line 279 of ETCoreMLModelProfiler.mm:

_outputNames = model.orderedOutputNames;

Analysis

The _outputNames ivar backs the outputNames property declared at line 228:

@property (readonly, copy, nonatomic) NSOrderedSet<NSString *> *outputNames;

Without this initialization, self.outputNames is nil. When set_model_outputs() (line 205-218) is called at line 320, it iterates over nil.count (which is 0 in Objective-C), producing an empty NSMutableArray. This empty array is then returned as modelOutputs from profileModelWithInputs at line 115 of ETCoreMLModelAnalyzer.mm, and any downstream code indexing into it will crash with NSRangeException.

Why the fix is correct

  1. Consistent with the debugger path. ETCoreMLModelDebugger receives outputNames explicitly via its initializer (line 555 of ETCoreMLModelDebugger.mm: _outputNames = [outputNames copy]), and the caller passes self.model.orderedOutputNames (line 132 of ETCoreMLModelAnalyzer.mm). The profiler already has the model reference, so pulling from model.orderedOutputNames directly is the right approach.

  2. Consistent with usage in the same file. self.outputNames is used at line 320 when calling set_model_outputs(outputFeatures, self.outputNames, modelOutputs) — this property must be initialized for the profiler to work correctly.

  3. orderedOutputNames on ETCoreMLModel is guaranteed non-nil — it's a readonly, copy property set during the model's own initialization.

  4. The PR author's explanation of why this wasn't caught earlier is accurate. When debug_buffer_size > 0, kIntermediateOutputs debug level enables both log_profiling_info and log_intermediate_tensors. Looking at executeModelWithInputs (lines 182-196 of ETCoreMLModelAnalyzer.mm), the debugModelWithInputs call at line 192 runs after profileModelWithInputs and overwrites outputs, masking the empty-array issue.

One minor note

The property is declared copy, but the direct ivar assignment _outputNames = model.orderedOutputNames; does not call copy. This is fine here — all other ivars in the same initializer block (lines 278-284) follow the same pattern of direct assignment without copy, which is standard practice in designated initializers. The copy semantics on the property only apply when using the property setter (dot syntax), not direct ivar access.

No additional changes needed. Clean, minimal fix.

@metascroy metascroy merged commit de61304 into pytorch:main Apr 10, 2026
164 of 188 checks passed
jpiat pushed a commit to jpiat/executorch that referenced this pull request Apr 14, 2026
… when debug_buffer_size=0 (pytorch#18763)

### Summary

`ETCoreMLModelProfiler::initWithModel:configuration:error:` does not
initialise the `_outputNames` property, leaving it as `nil`. This causes
an `NSRangeException` crash when `enable_etdump=True` with
`debug_buffer_size=0`.

**Root cause:**

When `_outputNames` is `nil`, `set_model_outputs()` iterates over
`nil.count` (0) and produces an empty `NSMutableArray`. This empty array
is returned as `modelOutputs` from `profileModelWithInputs`. Later,
`ETCoreMLModelManager` attempts to access `modelOutputs[0].shape` for
dynamic shape resizing, triggering:

NSRangeException: *** -[__NSArrayM objectAtIndexedSubscript:]: index 0
beyond bounds for empty array

This manifests as `RuntimeError: Caught an unknown exception!` at the
Python level.

**Why this was not caught earlier:**

When `debug_buffer_size > 0`, `kIntermediateOutputs` debug level is set,
which enables both `log_profiling_info` and `log_intermediate_tensors`.
In `ETCoreMLModelAnalyzer::executeModelWithInputs`, the
`debugModelWithInputs` path runs after profiling and overwrites the
empty array with valid outputs, masking the bug.

**Fix:**

Add `_outputNames = model.orderedOutputNames;` in the initialiser,
consistent with how `outputNames` is used in
`profilingInfoForOperationsAtPaths:` at line 319.

### Test plan

Tested on macOS 15.6 (arm64) with a CoreML model (input: 1x3x640x640, 3
outputs):

```python
from executorch.runtime import Runtime, Verification
import torch

rt = Runtime.get()
prog = rt.load_program("output_coreml.pte",
                        verification=Verification.Minimal,
                        enable_etdump=True,
                        debug_buffer_size=0)
method = prog.load_method("forward")
out = method.execute(torch.randn(1, 3, 640, 640))  # Previously crashed
prog.write_etdump_result_to_file("profile.etdump", "debug.bin")

┌──────────────────────────────────┬────────────────────────┬──────────────────────────────────┐
│             Scenario             │         Before         │              After               │
├──────────────────────────────────┼────────────────────────┼──────────────────────────────────┤
│ debug_buffer_size=0              │ NSRangeException crash │ Success, ETDump with timing data │
├──────────────────────────────────┼────────────────────────┼──────────────────────────────────┤
│ debug_buffer_size>0 (sufficient) │ Success (bug masked)   │ Success                          │
├──────────────────────────────────┼────────────────────────┼──────────────────────────────────┤
│                                  │                        │                                  │
└──────────────────────────────────┴────────────────────────┴──────────────────────────────────┘

cc @kimishpatel @YifanShenSZ @cymbalrush @metascroy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunk CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. module: coreml Issues related to Apple's Core ML delegation and code under backends/apple/coreml/ release notes: apple Changes to the Apple backend delegate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants