Skip to content

Fix crashes, wrong outputs and lowering failures in several backends and in exir - #24298

Open
pytorchbot wants to merge 9 commits into
release/1.6from
cherry-pick-24222-by-pytorch_bot_bot_
Open

pytorchbot wants to merge 9 commits into
release/1.6from
cherry-pick-24222-by-pytorch_bot_bot_

Conversation

@pytorchbot

@pytorchbot pytorchbot commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes found while exporting models to ExecuTorch backends.

Fixes

  • exir: constant_prop_pass keeps tensors that in-place ops write, also through views or in no_grad/autocast blocks. to_backend keeps the output node last. to_edge lowers torch._check_tensor_all and torch.sym_ite. GroupBasedPartitioner no longer makes partitions that depend on each other, unless caller node groups already do. Tensor constants get static shapes; deduplicated constants keep their data.
  • AOTI (AOTInductor) and CUDA: the missing-kernel check catches scatter and index_put fallbacks. detach lowers, also inside torch.cond. Strided outputs are copied. Transposed convolutions (1-D, 2-D, no dilation) and torch._check_tensor_all lower.
  • XNNPACK: the quantizer skips bfloat16 tensors, and cat respects scoped configs. ELU is delegated when XNNPACK can run it.
  • Qualcomm: autocast regions are inlined correctly. Unquantized bfloat16 nodes, data-dependent shapes and HTP partitions without runtime inputs stay on the CPU. Fixes for stacks, 0-d selects, RMSNorm and int64 constants.
  • OpenVINO: model data stays alive while OpenVINO 2026.4 reads it. Partitions are converted with their input types.
  • Core ML: bfloat16 ops, and ops above rank 5 outside static layout chains, stay on the CPU.
  • MLX, Arm, Cortex-M, Vulkan, NXP, portable: dtype checks for MLX tensor lists, one constant slot per weight, slice updates on a constant base; TOSA keeps unused delegate inputs for VGF; MatmulToBmmPass takes a constant operand; Vulkan constants above 4 dims; non-strict programs in the NXP quantizer; cummin.out.

This pick also carries a follow-up from #24316: on HTP, an op that reads only constants is decomposed. Without it, a constant-only partition that holds an op QNN keeps whole, such as linear or hardswish, stays on the CPU undecomposed, and to_edge_transform_and_lower fails with was not decomposed or delegated.

Behavior changes

  • A CUDA .pte loads only the weights it was exported with. A .ptd with other weights, for example after fine-tuning, needs a newly exported .pte.
  • CUDA graphs turn off when outputs do not match the ETensor layout. If only the capture call sees such a layout, that call returns an error.
  • GroupBasedPartitioner is about 2x slower on large graphs, and some graphs get a different number of partitions. to_backend keeps the original order of top-level nodes.
  • XNNPACK: weight-only quantized linears, and 8da4w or 8da8w linears (8-bit dynamic activations, 4-bit or 8-bit weights) lowered with to_backend, fail at export instead of giving wrong results. Delegated ELU flushes subnormal values to zero. Cats outside a scoped quantizer config are no longer quantized.
  • Ethos-U raises an error for a delegate with unused inputs instead of returning wrong values.
  • On HTP, an op that reads only constants is decomposed, also when its partition stays delegated. Quantize and dequantize ops stay whole.

We would like to cherry-pick this PR to release/1.6 after it merges. It needs #23915 there first.

Testing

Each fix has a test that fails without it. Run on an A100 (CUDA, Vulkan), an x86 CPU (OpenVINO, QNN SDK), macOS (Core ML, MLX), the TOSA reference model and VGF emulation (Arm), and the eIQ Neutron SDK (NXP). Not run: Metal, ROCm, Windows CUDA.

cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani

…and in exir (#24222)

Fixes found while exporting models to ExecuTorch backends.

## Fixes

- **exir:** `constant_prop_pass` keeps tensors that in-place ops write,
also through views or in `no_grad`/`autocast` blocks. `to_backend` keeps
the output node last. `to_edge` lowers `torch._check_tensor_all` and
`torch.sym_ite`. `GroupBasedPartitioner` no longer makes partitions that
depend on each other, unless caller node groups already do. Tensor
constants get static shapes; deduplicated constants keep their data.
- **AOTI (AOTInductor) and CUDA:** the missing-kernel check catches
scatter and index_put fallbacks. `detach` lowers, also inside
`torch.cond`. Strided outputs are copied. Transposed convolutions (1-D,
2-D, no dilation) and `torch._check_tensor_all` lower.
- **XNNPACK:** the quantizer skips bfloat16 tensors, and `cat` respects
scoped configs. ELU is delegated when XNNPACK can run it.
- **Qualcomm:** autocast regions are inlined correctly. Unquantized
bfloat16 nodes, data-dependent shapes and HTP partitions without runtime
inputs stay on the CPU. Fixes for stacks, 0-d selects, RMSNorm and int64
constants.
- **OpenVINO:** model data stays alive while OpenVINO 2026.4 reads it.
Partitions are converted with their input types.
- **Core ML:** bfloat16 ops, and ops above rank 5 outside static layout
chains, stay on the CPU.
- **MLX, Arm, Cortex-M, Vulkan, NXP, portable:** dtype checks for MLX
tensor lists, one constant slot per weight, slice updates on a constant
base; TOSA keeps unused delegate inputs for VGF; `MatmulToBmmPass` takes
a constant operand; Vulkan constants above 4 dims; non-strict programs
in the NXP quantizer; `cummin.out`.

## Behavior changes

- A CUDA `.pte` loads only the weights it was exported with. A `.ptd`
with other weights, for example after fine-tuning, needs a newly
exported `.pte`.
- CUDA graphs turn off when outputs do not match the ETensor layout. If
only the capture call sees such a layout, that call returns an error.
- `GroupBasedPartitioner` is about 2x slower on large graphs, and some
graphs get a different number of partitions. `to_backend` keeps the
original order of top-level nodes.
- XNNPACK: weight-only quantized linears, and 8da4w or 8da8w linears
(8-bit dynamic activations, 4-bit or 8-bit weights) lowered with
`to_backend`, fail at export instead of giving wrong results. Delegated
ELU flushes subnormal values to zero. Cats outside a scoped quantizer
config are no longer quantized.
- Ethos-U raises an error for a delegate with unused inputs instead of
returning wrong values.

We would like to cherry-pick this PR to release/1.6 after it merges. It
needs #23915 there first.

## Testing

Each fix has a test that fails without it. Run on an A100 (CUDA,
Vulkan), an x86 CPU (OpenVINO, QNN SDK), macOS (Core ML, MLX), the TOSA
reference model and VGF emulation (Arm), and the eIQ Neutron SDK (NXP).
Not run: Metal, ROCm, Windows CUDA.

(cherry picked from commit 054de1f)
@pytorch-bot

pytorch-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

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

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

✅ You can merge normally! (1 Unrelated Failure)

As of commit d59b402 with merge base 4db778e (image):

BROKEN TRUNK - The following job 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 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 29, 2026
@github-actions github-actions Bot added ciflow/trunk module: arm Issues related to arm backend labels Oct 29, 2026
@pytorchbot
pytorchbot deployed to upload-benchmark-results October 29, 2026 17:33 — with GitHub Actions Active
This branch was picked before #24263 and #24295 reached release/1.6, so
three files carried their content only in part. Take release/1.6 and use
the versions a clean pick of 054de1f gives for
backends/qualcomm/partition/qnn_partitioner.py,
backends/qualcomm/tests/test_passes.py and exir/tests/test_pass_infra.py.
@JacobSzwejbka
JacobSzwejbka deployed to upload-benchmark-results October 31, 2026 07:48 — with GitHub Actions Active
shoumikhin added a commit that referenced this pull request Oct 31, 2026
The pick was made on release/1.6, which does not have #24298 (the #24222
pick) yet. #24294 builds on #24222, so the pick depends on code that
release/1.6 lacks, such as _nodes_written_in_place in exir's
constant_prop_pass, and its conflict resolution brought in some of
#24222's OpenVINO changes but not others. This merges a clean pick of
7b14148 onto #24298's branch and takes the clean pick's side in the
OpenVINO files, so that, on top of #24298, the change is exactly #24294
(7b14148). A plain merge of the two would keep two copies of
_PRECISIONS in preprocess.py.
@shoumikhin

Copy link
Copy Markdown
Contributor

@claude /pr-review

@claude

claude Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

No blockers (Approve); the main follow-ups are a QNN lowering error when a dropped constant-only HTP partition contains a preserved op, a CUDA graph capture call that fails instead of running eagerly, and a possible, unverified QNN regression in Remove0DTensor worth checking before the release.

PR Review: #24298

Summary

This cherry-picks #24222 into release/1.6. It fixes crashes, wrong outputs and lowering failures in the CUDA/AOTI, QNN, Core ML, Arm, XNNPACK, OpenVINO, MLX, NXP, Vulkan and Cortex-M backends and in exir, and adds a portable cummin kernel. I found no blockers. Most of the problems below are edge cases that the new fixes create or don't cover. One change, in Remove0DTensor, might be a regression, but I could not run it to check.

How this was reviewed:

  • The shallow checkout has no merge base with origin/release/1.6, so I diffed against ba053ba, HEAD's release/1.6 parent. That diff has the same 85 files.
  • I spot-checked the test files rather than reading them line by line.
  • Low-confidence items are marked as such.

Code Quality

  • QNN: dropping an HTP partition with no runtime input can make lowering fail (backends/qualcomm/partition/qnn_partitioner.py:276-279, 313-337).

    • _drop_partitions_without_runtime_inputs runs after ops_to_not_decompose (:393-402) has already kept skip-table ops such as linear, stack and rms_norm whole. filter_fn (backends/qualcomm/partition/utils.py:203) does not check whether a node's inputs are all parameters.
    • If a dropped partition contains one of these ops, the op is left neither decomposed nor delegated. to_edge_transform_and_lower then raises "was not decomposed or delegated" from _sanity_check_graph_for_non_decomp_ops(..., generate_error=True) (exir/program/_program.py:1429-1436).
    • Trigger: a linear or stack whose inputs are all parameters or buffers, and whose result reaches the rest of the graph only through a CPU-only op.
    • This is not a regression, since HTP compile failed here before. test_partitioner_keeps_constant_only_subgraph_on_cpu (test_passes.py:944) covers only mul/sub/relu.
    • Suggested fix: deal with parameter-only subgraphs before the decomposition decision. Either constant-fold them, or wrap filter_fn so it returns False for nodes whose inputs are all parameters. Add a linear or stack case to the test.
  • CUDA graphs: a valid call fails when the first strided output appears at capture (backends/cuda/runtime/cuda_backend.cpp:1078-1093).

    • Capture returns Error::NotSupported, even though the CaptureGuard destructor has already disabled graphs. Rerunning eagerly would make the call succeed, which is what the warmup path does (:1184-1192).
    • test_strided_output_first_seen_at_capture (backends/cuda/tests/test_output_stride_rearrange.py:345-360) asserts the error for call 3.
  • QNN Remove0DTensor may now pass 0-d tensors to QNN (backends/qualcomm/_passes/remove_0d_tensor.py:28-33, 51-56). Suspected; I did not run it.

    • The pass used to replace every 0-d select with its input. Now it does so only when the input has one element and every user is add, sub, mul or div with an output of rank 1 or more.
    • The pass's docstring says QNN does not allow 0-d tensors. I found nothing in the partitioner or op builders that rejects rank-0 tensors, so the selects the pass now keeps may fail at QNN compile. At best they fall back to CPU.
    • Possible regression: a one-element tensor selected and fed to gt, where, pow, maximum or another broadcasting op outside the four-op list. The old pass rewrote these; the new one leaves them 0-d.
    • x * self.table[2], which gave wrong results before, now reaches QNN as a 0-d tensor.
    • test_remove_0d_tensor_keeps_select_it_cannot_replace (test_passes.py:438) runs only the pass, not a full lowering.
    • Suggested fix: when every user broadcasts, replace the select with a one-element slice_copy of shape [1], which also works for inputs with more than one element. Widen broadcast_ops to the other broadcasting elementwise ops. Or reject 0-d outputs in the partitioner so they fall back cleanly.
  • QNN autocast: the retrace decomposes ops QNN keeps whole (backends/qualcomm/_passes/decompose_wrap_with_autocast.py:60-67). Plausible; I did not run it.

    • Enabled regions are retraced with post-dispatch make_fx(submod, tracing_mode="fake"), which decomposes CompositeImplicitAutograd ops. For example, a 2-D linear becomes t + addmm, and DecomposeAddmm then turns that into mm + add.
    • A linear inside an autocast region therefore lowers as MatMul instead of FullyConnected, and the skip-decomp table never sees it.
    • test_decompose_wrap_with_autocast_nested_regions (test_passes.py:147) checks numerics only.
    • Suggested fix: keep skip-table ops during the retrace, as to_edge does for ops_to_not_decompose, and assert in the test that aten.linear survives.
  • AOTI: two more private Inductor methods are patched (backends/aoti/aoti_backend.py:183-186, 209-238).

    • CppWrapperCpu._generate_scatter_fallback and _generate_index_put_fallback are read unconditionally when the context is entered. If either is renamed, every CUDA and Metal preprocess fails with AttributeError, not only models that use scatter or index_put. This matters most on main, which tracks PyTorch nightly.
    • The self.writeline swap (:216-221) leaves an instance attribute behind after the restore. Taking len(self.lines) before the original call and scanning the lines added after it avoids the swap.
  • The shared-constant fix is incomplete (backends/transforms/utils.py:280-283). Low confidence.

    • delete_constant_placeholder now keeps a target that another placeholder from duplicate_constant_node still uses. erase_constant_node (exir/passes/constant_prop_pass.py:396-408) still pops state_dict/constants[name] unconditionally.
    • Its callers (Samsung constant prop, Qualcomm convert_mha_to_sha, Cadence) may hit the same problem. Consider the same input_specs check, or the currently_used_targets approach from exir/lowered_backend_module.py:1023-1060.
  • Constant propagation's alias walk only follows annotated views (exir/passes/constant_prop_pass.py:222-236). Low confidence.

    • When a training-IR program is reloaded, the fallback walk follows args[0] only through getitem and is_view ops. Aliasing composites without alias annotations, such as atleast_1d/2d/3d, meshgrid and broadcast_tensors, don't mark their base. A constant written in place through them could still be folded.
    • The new test covers only a slice write.

Testing

  • The Ethos-U unused-input check is only covered by a broad strict xfail (backends/arm/test/ops/test_copy.py:98-104).
    • copy_over_unused_input is xfailed with a bare RuntimeError, so any RuntimeError on u55/u85 (for example, a Vela failure) satisfies it. The new check in backends/arm/ethosu/backend.py:117-131 is never directly asserted.
    • Suggested fix: add a test with pytest.raises(RuntimeError, match="binds delegate arguments by position").

Performance

  • Core ML: one rejected node turns off preservation for the whole op (backends/apple/coreml/partition/coreml_partitioner.py:433-477).

    • A rejected node removes its op from do_not_decompose for every call site. For example, one bf16 or rank > 5 SDPA decomposes every SDPA in the model, and this is only logged on the exception path.
    • This is not a regression, since mixed models used to fail.
    • Suggested fix: at least self.log_once the op and node. Better, return a per-node filter, but that moves Core ML onto the EDGE_DO_NOT_DECOMP path.
  • CUDA: _conv_transpose_as_conv costs stride^n times the work (backends/cuda/cuda_backend.py:485-548).

    • Zero insertion followed by a stride-1 conv does stride^n times the MACs of a direct transposed conv, and it materializes the zero-filled input. That is 4× for a stride-2 2-D conv and 8× for a stride-8 1-D vocoder upsampler.
    • A polyphase split (one stride-1 conv per output phase, then interleave) avoids this. Fine as a follow-up.

Recommendation

Approve

  • Nothing above blocks the cherry-pick. The follow-ups belong on main.
  • Remove0DTensor is the only possible regression, and it is unverified. Before release, it is worth a quick QNN lowering check of a size-1 select feeding where or gt, and of x * self.table[2].

View job run

The TensorRT guide said that when a second export overwrites aoti_cuda_blob.ptd, the first model can return wrong answers with no error. CUDA weight keys now carry a hash of their contents, so the first model's weights are missing from the new file and loading fails instead. Docs only.
…r HTP

On HTP, the partitioner keeps a partition that reads nothing at runtime on
the CPU, because HTP refuses such a graph. That happens after
decomposition, so an op that QNN asks to keep whole, such as linear or
hardswish, could land on the CPU still undecomposed, and
to_edge_transform_and_lower failed with "was not decomposed or delegated".

The filter returned with the ops to keep whole now also leaves out any op
that reads only constants, so it is decomposed like other CPU ops. An op
that reads a runtime input is still kept whole. The constant-only
partition test now has a linear in its constant branch, and fails without
this change.
…ly op

The comment said ops that read only constants are decomposed like other
CPU ops. Partitions are not known when the filter is built, so the rule
also decomposes such ops whose partition stays delegated. The comment and
the ops_to_not_decompose docstring now say so. Comment only.
… HTP

The HTP rule that decomposes ops reading only constants also covered the
quantize and dequantize ops in the skip table, such as
dequantize_per_channel_group on a frozen weight. QNN builds the weight
encoding from those ops. With today's torch they are not decomposed
anyway, so nothing changed in practice, but the filter should not ask for
it. They are now kept whole, and ops after them still count as reading
only constants.

A new test checks the filter itself and needs no QNN SDK, so it runs in
CI: a constant-only linear is decomposed, a linear on an input and a
constant-only group-wise dequantize are kept whole.
After lowering, exir asks the partitioner for its filter again and
reports any kept-whole op still on the CPU. On HTP the filter now let
constant-only ops through, so that report went quiet. For example
RecomposeRmsNorm rebuilt a constant-only rms_norm that then stayed on the
CPU, and the program failed later in to_executorch with "Missing out
variants" instead of the clear error from before. On the lowered (edge)
program the filter is plain filter_fn again, so these cases report as
they did before the HTP change. The filter test checks this too.
@shoumikhin
shoumikhin deployed to upload-benchmark-results October 31, 2026 21:52 — with GitHub Actions Active
shoumikhin added a commit that referenced this pull request Nov 1, 2026
This pick is stacked on #24298 (the #24222 pick), which has since gained
follow-up commits for the Qualcomm partitioner and the TensorRT guide.
Taking them keeps this branch equal to #24298 plus #24294, so that
refreshing it from release/1.6 after #24298 merges cannot undo them. The
merge is clean, and the follow-ups touch no OpenVINO files.

This branch was successfully deployed

1 active and 1 inactive deployments
upload-benchmark-results — d59b4020 Deployed Oct 31, 2026 by shoumikhin via upload-benchmark-results #21685
cadence — d59b4020 Deployed Oct 31, 2026 by shoumikhin via hifi-op-test / hifi4 #35775
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: arm Issues related to arm backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants