Skip to content

Fix crashes and lowering failures in the AOTI, OpenVINO and Qualcomm backends - #24222

Open
shoumikhin wants to merge 5 commits into
pytorch:mainfrom
shoumikhin:fix-lerobot-export-bugs
Open

shoumikhin wants to merge 5 commits into
pytorch:mainfrom
shoumikhin:fix-lerobot-export-bugs

Conversation

@shoumikhin

@shoumikhin shoumikhin commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Five backend bugs, one commit each. AOTI is AOTInductor, the ahead-of-time compiler behind the CUDA and Metal backends.

CUDA and Metal: missing scatter and index_put kernels pass lowering

AotiBackend checks at lowering time that every kernel Inductor leaves to ATen exists in the backend's shim library. Scatter and index_put fallbacks write their call directly, so the check missed them and the program died at run time on an undefined symbol. Now the check also reads the kernel names they write.

OpenVINO 2026.4: loading a .pte from a file crashes

Since OpenVINO 2026.4 the compiled model can keep reading the model data, but OpenvinoBackend::init freed it right away. Loading from a file really frees it, so the first execute can crash. Now init frees it only before 2026.4, whose import copies it. The op tests now load from a file.

Qualcomm: DecomposeWrapWithAutocast fails on autocast regions

It matched an autocast submodule's inputs to the operands by name, but the names often differ (KeyError: 'arg2_1'). A nested region also silently ran the wrong code. Now it matches by position and inlines nested regions first.

CUDA and Metal: detach() cannot be lowered

Edge turns x.detach() into detach_copy, which Inductor cannot lower. The AOTI pass that maps slice_copy back to a view now also maps detach_copy to alias.

CUDA: a strided method output fails at run time

AOTInductor can return an output as a slice of a larger buffer, such as y[:, :, :14], but the output copy took only contiguous tensors ("SlimTensor must be contiguous"). The slow copy path now handles them. A CUDA graph replay copies raw bytes, which is wrong for these and for outputs whose ETensor has another dim order, so such methods now run without a graph.

Testing

  • CUDA (A100, torch nightly): test_detach, test_missing_scatter_and_index_put_kernels_fail_at_export and CopiesAStridedView fail without the fixes and pass with them; the rest of test_cuda_export.py and test_sort_shim.py pass. A Linear followed by a slice now runs and matches eager, also with CUDA graphs on. Metal was not run.
  • OpenVINO, x86 CPU: with 2026.4.1 the op tests crash without the fix, and with it give the same results as loading from a buffer (some subtests fail on this CPU either way). With 2026.3.0 they run and init still frees the data.
  • Qualcomm, QNN SDK build: the new pass test fails without the fix and passes with it.

Copilot AI balanced review requested due to automatic review settings October 28, 2026 03:40
@pytorch-bot

pytorch-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

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

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

❌ 1 New Failure, 2 Pending, 2 Unrelated Failures

As of commit 55b4840 with merge base 92721a8 (image):

NEW FAILURE - The following job has failed:

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

BROKEN TRUNK - The following job 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-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 Oct 28, 2026

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude /pr-review

@claude

claude Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 22m 42s —— View job


Approve: nothing blocks. The main open point is that on OpenVINO before 2026.4, the delegate blob now stays in memory next to OpenVINO's own copy for the whole life of the method. Three smaller robustness and test gaps are noted below.

PR Review: #24222

Summary

The PR does two things:

  • AOTI lowering now reports missing scatter and index_put fallback kernels.
  • OpenvinoBackend::init no longer frees a blob that the compiled model may still read.

Nothing blocks merging. The OpenVINO memory cost on versions before 2026.4 is worth settling, and three smaller points follow. The PyTorch Inductor and OpenVINO sources are not in this checkout, so I mark statements about their internals as low confidence. I ran no tests.

Code Quality

  • backends/aoti/aoti_backend.py:208-237: the scatter check rebuilds Inductor's private symbol name instead of checking what Inductor writes.

    • The other three wrappers check a name that Inductor passes in. This one copies the naming logic inside CppWrapperCpu._generate_scatter_fallback (get_c_shim_func_name(...).replace("__", "_") + "_out", lines 220-223). It also spells out all 8 of that method's parameters, so a signature change turns every scatter fallback into a TypeError.
    • The test asserts the same rebuilt string (backends/cuda/tests/test_cuda_export.py:580).
      • If a torch bump changes the name Inductor emits, CI stays green, but the check reports a name the .wrapper.so never calls.
      • Once someone adds that name to get_supported_fallback_kernels, lowering passes and the program crashes on its first run again.
    • Comparing against what the original method writes would catch this drift at lowering time. For example, wrap self.writeline for the duration of the call, and fail if the emitted call is not kernel.
    • Low confidence: one check over the aoti_torch_* calls in the generated wrapper would also cover direct-emission paths that nothing watches yet. The CUDA shim defines no aoti_torch_item_float16 or aoti_torch_item_float64 (backends/cuda/runtime/shims/memory.cpp:451-458), and I believe Inductor's codegen_tensor_item emits those calls directly.

    Fix this →

  • backends/openvino/tests/ops/base_openvino_op_test.py:71-80: the mapped .pte is released only on success.

    • If load_method, execute, or the assert raises, the traceback keeps program and method alive while TemporaryDirectory cleans up.
    • On Windows (the case the comment on line 79 is about), the cleanup then raises PermissionError on top of the real failure.
    • tempfile.TemporaryDirectory(ignore_cleanup_errors=True) avoids this; pyproject.toml requires Python 3.11 or newer, so it is available.

    Fix this →

Testing

  • backends/aoti/aoti_backend.py:251-256: the Metal part of fix 1 has no test.

    • The PR says Metal was not run, and nothing under backends/apple/metal/tests/ uses scatter, index_put, or the missing-kernel error.
    • The patch is on CppWrapperCpu. The CUDA test shows that the CUDA wrapper picks it up, but nothing shows that the MPS wrapper does not override _generate_scatter_fallback or _generate_index_put_fallback. This part is low confidence, since I could not read Inductor.
    • If the MPS wrapper does override them, Metal programs still lower and then crash on their first run, because the .so is opened with RTLD_LAZY (backends/apple/metal/runtime/metal_backend.cpp:347).
    • Adding the Int64Accumulate case to backends/apple/metal/tests/test_metal_backend.py, built with export_model_to_metal (test_modules.py:1218), would cover this.

    Fix this →

Performance

  • backends/openvino/runtime/OpenvinoBackend.cpp:336-337: on OpenVINO before 2026.4, the blob now stays alive next to OpenVINO's own copy for the life of the method.

    • The PR description accepts this cost, but neither the code comment nor the docs mention it.
    • 2025.1 to 2026.3 are still advertised as supported. docs/source/build-run-openvino.md:64 and the runtime-not-found error at OpenvinoBackend.cpp:215 both say openvino>=2025.1.0,<2027.0.0.
    • What the extra copy costs depends on the loader:
      • With the default Module load mode, LoadMode::File (extension/module/module.h:100), it is a heap buffer the size of the compiled model.
      • With the LLM runner's default, MmapUseMlockIgnoreErrors (extension/llm/runner/llm_runner_helper.h:112), the pages stay locked in RAM whenever mlock succeeds.
    • For the examples/openvino/llama and qwen2_5 exports, the blob is the whole quantized model.
    • Freeing only on versions before 2026.4 gives those versions back exactly the behavior they had before this PR. This needs ov_get_openvino_version and ov_version_free declared in OpenvinoApi.h and loaded with the other LOAD_SYMs. At minimum, state the cost in the comment and on the docs page.

    Fix this →

Recommendation

Approve

Nothing here blocks the merge. The one point to settle is the OpenVINO memory cost: either free the blob on versions before 2026.4, or document the cost. That can happen here or in a follow-up.


  • Gather context and fetch the diff (the checkout is shallow, so I compared against the merge base 8ecf0fb)
  • Run three independent reviews, then consolidate and fact-check them
  • Post review (I ran no tests)

…kends

An AOTI backend (CUDA, Metal) checks at lowering time that every kernel
Inductor leaves to ATen exists in the backend's shim library, so a missing
one fails the export with a clear list. The check watches two code paths
that emit a C shim call, but scatter and index_put fallbacks write their
call directly. A program that needs one of those kernels lowers without an
error, and the process then dies on the first run with an undefined symbol.

For example, Inductor leaves an int64 scatter_add_ to
aoti_torch_cuda_scatter_reduce_two_out, which the CUDA shim does not have.

Watch those two paths too. They do not pass the kernel name to a method
the check can see, so read it from the line they write. This checks the
name the generated code really calls.
Since OpenVINO 2026.4, ov_core_import_model wraps the blob it is given in
an ov::Tensor and the compiled model can keep reading that memory. Earlier
versions read it through a stream during the import only.
OpenvinoBackend::init frees the blob right after the import. When the
program is loaded from a buffer, Free() does nothing, so nothing goes
wrong. When it is loaded from a file, the blob is really freed: with a
memory-mapped file the first execute crashes with a segmentation fault,
even for a two-layer model, and with a file read into memory the model
reads freed memory.

Free the blob in init only when the OpenVINO runtime is older than 2026.4,
as before. With newer versions keep it: ExecuTorch owns it and frees it
after destroy(), so it lives exactly as long as the compiled model.

The op tests loaded every program from a buffer, so they never saw this.
They now load from a file, as most users do.
…gions

torch.export records a `with torch.autocast(...)` block as a
wrap_with_autocast call on a submodule. DecomposeWrapWithAutocast inlines
that submodule into the graph. It had two bugs:

1. It mapped the submodule's placeholders to the operands by name. When
   the region reads inputs or parameters, the placeholders are named like
   arg2_1, arg0_1, arg1_1, not after the operands, so the pass failed with
   a KeyError such as 'arg2_1'. For example:

       with torch.autocast("cpu", enabled=False):
           y = self.linear(x)

2. It only looked at the top graph. A region nested in another region was
   copied out with its get_attr, whose submodule name only means something
   inside the outer submodule. In the top module it resolved to the outer
   body, so the program silently ran the wrong code.

Map placeholders to operands by position, and inline nested regions
before their parent.
Edge turns `x.detach()` into `aten.detach_copy`. Inductor has no lowering
or C shim for it, so an AOTI backend (CUDA, Metal) cannot lower any graph
that calls detach, for example:

    return (x * 2).detach() + 1

fails with "missing fallback kernels: aten::detach_copy".

ReplaceViewCopyWithViewPass already maps copy ops Inductor cannot lower
(slice_copy, select_copy, split_copy) back to views. Map detach_copy to
alias, which is what detach means without autograd. Mapping to detach
itself would still fail Edge verification under the default compile
config, because detach is a non-core view op that the partitioner
preserves.
AOTInductor can return a method output as a view into a larger buffer
with strides that do not match a contiguous tensor, for example a slice
`y[:, :, :14]` of a [1, 50, 32] result. The CUDA backend copies outputs
into the planned ETensor only from contiguous SlimTensors, so execute()
failed with "SlimTensor must be contiguous" although lowering accepted
the program.

The copy already has a slow path that rearranges elements through host
memory when the strides differ from the ETensor's. Let it take
non-contiguous views too: copy the span the view reaches instead of
nbytes(), and check that span against the storage, with checked
arithmetic. Wrapping a SlimTensor without a copy still requires a
contiguous one.

A CUDA graph replay copies each output's raw bytes. For such a strided
output, and also for a contiguous output whose ETensor uses another dim
order, the replay was silently wrong. A raw copy is known to be right
when both are contiguous in the default dim order, so for any other
layout warmup now turns graphs off and the capture step refuses to
replay, so the method runs without a graph.
@shoumikhin
shoumikhin force-pushed the fix-lerobot-export-bugs branch from 35f7db1 to 55b4840 Compare October 28, 2026 09:44
@shoumikhin
shoumikhin requested a review from psiddh as a code owner October 28, 2026 09:44
Copilot AI balanced review requested due to automatic review settings October 28, 2026 09:44
@shoumikhin shoumikhin changed the title Fix two backend crashes that show up only when a program runs Fix crashes and lowering failures in the AOTI, OpenVINO and Qualcomm backends Oct 28, 2026

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. release notes: backends

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants