Skip to content

Validate 4D input tensor memory layout against method metadata. - #22048

Open
ArchitAnant wants to merge 1 commit into
pytorch:mainfrom
ArchitAnant:fix/binding_input_channel_4d
Open

Validate 4D input tensor memory layout against method metadata.#22048
ArchitAnant wants to merge 1 commit into
pytorch:mainfrom
ArchitAnant:fix/binding_input_channel_4d

Conversation

@ArchitAnant

Copy link
Copy Markdown

Summary

Current runtime accepts either contiguous or channel_last tensor unconditionally. This produces silent numerical corruption if a method was exported assuming contiguous strides and a channels-last tensor is passed at runtime (and vice versa).

This patch adds a 4D tensor layout validation check against the method metadata at runtime. If the input tensor layout does not match the expected layout, an error is raised.

Fixes: #21837
cc @john-rocky

Test plan

Manually tested for both export configurations:

import torch
from executorch.exir import to_edge_transform_and_lower
from executorch.runtime import Runtime


class M(torch.nn.Module):
    def forward(self, x):
        return x * x


contig = torch.arange(12, dtype=torch.float32).reshape(1, 3, 2, 2)
chlast = contig.to(memory_format=torch.channels_last)
assert torch.equal(contig, chlast)

ep = torch.export.export(M(), (contig,))
open("/tmp/r.pte", "wb").write(
    to_edge_transform_and_lower(ep, partitioner=[]).to_executorch().buffer)
m = Runtime.get().load_program("/tmp/r.pte").load_method("forward")

try:
    print("expected     :", (contig ** 2).flatten().tolist())
    print("contiguous   :", m.execute([contig])[0].flatten().tolist())
    print("channels_last:", m.execute([chlast])[0].flatten().tolist())
except Exception as e:
    print("[ERROR]", e)

ep = torch.export.export(M(), (chlast,))
open("/tmp/r.pte", "wb").write(
    to_edge_transform_and_lower(ep, partitioner=[]).to_executorch().buffer)
m = Runtime.get().load_program("/tmp/r.pte").load_method("forward")

try:
    print("expected     :", (chlast ** 2).flatten().tolist())
    print("channels_last:", m.execute([chlast])[0].flatten().tolist())
    print("contiguous   :", m.execute([contig])[0].flatten().tolist())
except Exception as e:
    print("[ERROR]", e)

output:

expected     : [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
contiguous   : [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
[ERROR] Input 0 for method forward expected contiguous memory layout but received a different layout.
expected     : [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
channels_last: [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
[ERROR] Input 0 for method forward expected channels-last memory layout but received a different layout.

Copilot AI lite review requested due to automatic review settings August 22, 2026 09:15
@pytorch-bot

pytorch-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⚠️ 15 Awaiting Approval

As of commit edc74e6 with merge base fbd4bbf (image):

AWAITING APPROVAL - The following workflows need approval before CI can run:

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

@linux-foundation-easycla

linux-foundation-easycla Bot commented Aug 22, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

  • ✅ login: ArchitAnant / name: Archit Anant (408fe80)

@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 Aug 22, 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.

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.

Pull request overview

Adds runtime-side validation in the Python bindings (portable mode) to ensure 4D tensor memory layout passed at execution matches the layout encoded in the exported method metadata, preventing silent numerical corruption when contiguous vs channels-last are mismatched.

Changes:

  • Fetches input tensor dim_order from method metadata and validates incoming tensors against the expected contiguous vs channels-last layout.
  • Updates tensor conversion logic to reject layout mismatches with a runtime error.
Suppressed comments (2)

extension/pybindings/pybindings.cpp:846

  • The error message "Input ... expects an unsupported memory format" is confusing/grammatically incorrect (the method metadata is what expects a layout). Consider rephrasing to clarify that the expected layout encoded in metadata is unsupported, and (optionally) mention what layouts are supported.
          throw std::runtime_error(
              "Input " + std::to_string(i) + " for method " + method_name +
              " expects an unsupported memory format.");

extension/pybindings/pybindings.cpp:1251

  • Same message clarity issue here: "Input ... expects an unsupported memory format" reads as if the input expects something. It would be clearer to say the method metadata encodes an unsupported expected layout (and mention supported layouts).
          throw std::runtime_error(
              "Input " + std::to_string(i) + " for method " + method_name +
              " expects an unsupported memory format.");

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread extension/pybindings/pybindings.cpp Outdated
dim != 4) {
throw std::runtime_error(
"Input " + std::to_string(i) + " for method " + method_name +
" expected channels-last memory layout but recvied a different layout.");
Current runtime accepts either contiguous or channel_last tensor
unconditionally. This produces silent numerical corruption if a method
was exported assuming contiguous strides and a channels-last tensor is
passed at runtime (and vice versa).

This patch adds a 4D tensor layout validation check against the method
metadata at runtime. If the input tensor layout does not match the
expected layout, an error is raised.

Fixes pytorch#21837

Signed-off-by: Archit Anant <architanant5@gmail.com>
@ArchitAnant
ArchitAnant force-pushed the fix/binding_input_channel_4d branch from 408fe80 to edc74e6 Compare August 22, 2026 09:28
Copilot AI review requested due to automatic review settings August 22, 2026 09:28

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.

Pull request overview

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

Suppressed comments (3)

extension/pybindings/pybindings.cpp:801

  • module_->method_meta(method_name) is invariant across inputs but is recomputed for every tensor input. Consider fetching MethodMeta once per run_method call (before the inputs loop) and reusing it to avoid repeated metadata lookups.
        auto method_meta_res = module_->method_meta(method_name);
        if (!method_meta_res.ok()) {
          throw std::runtime_error(
              "Failed to get metadata for method " + method_name);
        }

extension/pybindings/pybindings.cpp:1251

  • Same ambiguity as in PyModule::run_method: this error reads like the input is unsupported, but it’s actually the exported method’s expected dim_order that the Python bindings don’t handle. Clarify that only contiguous and channels-last layouts are supported by these bindings.
          throw std::runtime_error(
              "Input " + std::to_string(i) + " for method " + method_name +
              " expects an unsupported memory format.");

extension/pybindings/pybindings.cpp:1235

  • This change alters runtime behavior by rejecting mismatched contiguous vs channels-last inputs. Please add a regression unit test (e.g., in runtime/test/test_runtime.py) that exports once with contiguous and once with channels-last 4D inputs and asserts the opposite layout raises with the new error message (covering the issue #21837 repro path via Runtime.load_program(...).load_method(...).execute(...)).
        if (expected_contiguous) {
          if (!at_tensor.is_contiguous()) {
            throw std::runtime_error(
                "Input " + std::to_string(i) + " for method " + method_name +
                " expected contiguous memory layout but received a different layout.");

Comment on lines +797 to +806
auto method_meta_res = module_->method_meta(method_name);
if (!method_meta_res.ok()) {
throw std::runtime_error(
"Failed to get metadata for method " + method_name);
}
auto tensor_meta_res = method_meta_res.get().input_tensor_meta(i);
if (!tensor_meta_res.ok()) {
throw std::runtime_error(
"Failed to get tensor metadata for input " + std::to_string(i));
}
Comment on lines +1207 to +1211
auto tensor_meta_res = method_->method_meta().input_tensor_meta(i);
if (!tensor_meta_res.ok()) {
throw std::runtime_error(
"Failed to get tensor metadata for input " + std::to_string(i));
}
Comment on lines +844 to +846
throw std::runtime_error(
"Input " + std::to_string(i) + " for method " + method_name +
" expects an unsupported memory format.");
@john-rocky

Copy link
Copy Markdown
Contributor

Thanks for picking this up, @ArchitAnant — I said I'd run the repro against the fix, so here
it is, built from source on the PR's own parent commit so the only thing that moves between
the two arms is extension/pybindings/pybindings.cpp.

Build: fbd4bbfd8 (the commit this PR sits on) as the control, then
git checkout <pr> -- extension/pybindings/pybindings.cpp and rebuild. Only that one file
differs; the _C extension's SHA changes, so the patch definitely reached the binary.
macOS arm64, EXECUTORCH_BUILD_PYBIND=ON, backends off, portable kernels.

Your repro, both arms

=== control (fbd4bbfd8) ===
expected     : [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
contiguous   : [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
channels_last: [0.0, 16.0, 64.0, 1.0, 25.0, 81.0, 4.0, 36.0, 100.0, 9.0, 49.0, 121.0]   <- the bug
expected     : [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
channels_last: [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
contiguous   : [0.0, 9.0, 36.0, 81.0, 1.0, 16.0, 49.0, 100.0, 4.0, 25.0, 64.0, 121.0]   <- the bug

=== patched (this PR) ===
expected     : [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
contiguous   : [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
[ERROR] Input 0 for method forward expected contiguous memory layout but received a different layout.
expected     : [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
channels_last: [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
contiguous   : [0.0, 9.0, 36.0, 81.0, 1.0, 16.0, 49.0, 100.0, 4.0, 25.0, 64.0, 121.0]

Wrong numbers become an error, in both directions. Confirmed.

Wider battery — both binding paths

The patch changes two call sites, and the test plan above exercises one, so I ran everything
through Runtime.load_program(...).load_method(...) (PyMethod) and
_load_for_executorch(...) (PyModule). Twelve cases x two APIs; control in brackets.

case control patched
4D contiguous method, contiguous input ok ok
4D contiguous method, channels-last input silently wrong rejected
4D channels-last method, channels-last input ok ok
4D channels-last method, contiguous input silently wrong rejected
2D method, contiguous input ok ok
2D method, transposed input rejected rejected
4D contiguous method, strided (gapped) 4D view rejected rejected
4D C=1 method, channels-last input (identical strides) ok ok
two inputs, both contiguous ok ok
two inputs, second channels-last silently wrong rejected, names Input 1
4D method, 3D input rejected (0x10) rejected (0x10)
5D method, contiguous input ok ok

Both APIs agree on every row. Nothing that used to work stops working, the 2-D transposed
case that the old check already caught is still caught, and the multi-input case points at
the right index.

One case worth knowing about (not a regression)

A method exported from a C=1 channels-last tensor records
dim_order = [0, 2, 1, 3] — neither {0,1,2,3} nor {0,2,3,1}:

shape (1,1,2,2): contiguous strides (4, 4, 2, 1) | channels_last strides (4, 1, 2, 1)
sq_thin_cl.pte: sizes=[1, 1, 2, 2] dim_order=[0, 2, 1, 3]

With C=1 the two layouts tie on dims 1 and 3, and the stride sort breaks the tie the other
way. Your else branch therefore fires for such a method, for every input — including
the very tensor it was exported from. I checked the control before calling this a
regression, and it is not one: that method is already uncallable there, it just fails with
an opaque method->execute() failed with error 0x12. So this PR makes the failure legible
rather than causing it. Flagging it only because it means the else branch is reachable in
practice, not just in theory.

Cost per call

The check does a method_meta() / input_tensor_meta(i) lookup per input per execute(),
so I measured it — control and patched interleaved twice on an idle machine, median of five
runs of 4000 calls each:

control patched
1 input, load_method 1.52 / 1.52 us 1.52 / 1.56 us
1 input, _load_for_executorch 1.49 / 1.50 us 1.49 / 1.52 us
2 inputs, load_method 1.73 / 1.76 us 1.78 / 1.78 us
2 inputs, _load_for_executorch 1.78 / 1.78 us 1.83 / 1.83 us

Roughly 0.05 us per extra input, which is inside the run-to-run spread and invisible next to
any real model's execution. No objection from me on that front.

One question

The new check sits inside the #else of #ifdef USE_ATEN_LIB, so an ATen-mode build does
not get it. I have not measured whether the original bug exists there — in ATen mode the
at::Tensor is passed through with its own strides, so I would guess not, but that is a
guess and not something I tested.

Everything above is on portable kernels with partitioner=[]; I did not test with a backend
partitioner or the upstream test suite.

@ArchitAnant

ArchitAnant commented Aug 24, 2026

Copy link
Copy Markdown
Author

@john-rocky Thank you so much for running this through such a rigorous battery of tests!

One question

The new check sits inside the #else of #ifdef USE_ATEN_LIB, so an ATen-mode build does not get it. I have not measured whether the original bug exists there — in ATen mode the at::Tensor is passed through with its own strides, so I would guess not, but that is a guess and not something I tested.

Everything above is on portable kernels with partitioner=[]; I did not test with a backend partitioner or the upstream test suite.

I would assume that as well, at::Tensor with its full dynamic stride-handling intact should not contain this bug.

Just one question, in your initial test log:

...
=== patched (this PR) ===
expected     : [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
contiguous   : [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
[ERROR] Input 0 for method forward expected contiguous memory layout but received a different layout.
expected     : [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
channels_last: [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0, 100.0, 121.0]
contiguous   : [0.0, 9.0, 36.0, 81.0, 1.0, 16.0, 49.0, 100.0, 4.0, 25.0, 64.0, 121.0]

^^^ shouldn't this also be an error?

From your tests I see the patch works fine. So, I am assuming it's just a typo? Just curious.

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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Runtime accepts a channels-last input for a contiguous method and reads it as contiguous (silent wrong output)

4 participants