pytorch / pytorch/executorch

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

Open
#21,837 3 comments 0 reactions 1 assignee View on GitHub

@JakeStevens is already working on this.

Since Aug 14, 2026.

bug high priority module: runtime triage review
Dominant language
Python
Stars
5k
Forks
1.2k
Avg merge
2d 10h
Merged PRs (30d)
581

Description

The runtime validates input layout — a transposed 2-D tensor is rejected with
Input 0 for method forward should be contiguous or channels-last. But a
channels-last 4-D tensor is accepted even when the method was exported with a
contiguous input, and its data is then read in contiguous order. No error, no
warning, wrong numbers.

Repro (executorch 1.4.0, torch 2.13, macOS arm64, portable kernels — the XNNPACK
partitioner makes no difference):

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 * 2.0


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")

print("expected     :", (contig * 2).flatten().tolist())
print("contiguous   :", m.execute([contig])[0].flatten().tolist())
print("channels_last:", m.execute([chlast])[0].flatten().tolist())
expected     : [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0]
contiguous   : [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0]
channels_last: [0.0, 8.0, 16.0, 2.0, 10.0, 18.0, 4.0, 12.0, 20.0, 6.0, 14.0, 22.0]

Two tensors that torch.equal reports as equal produce different results.

Why this is worth fixing rather than documenting. Channels-last is what you
get from the most common way to build an image input:

torch.from_numpy(hwc_array.transpose(2, 0, 1)[None])   # NCHW shape, NHWC strides

np.transpose returns a view and astype keeps memory order by default
(order='K'), so this is easy to hit without ever naming channels_last.

The failure mode is what makes it expensive. torch.randn inputs are contiguous
and match the eager model bit-exactly, so a parity check passes; only real images
diverge. I spent an afternoon bisecting eager -> ep.module() ->
edge.exported_program().module() -> .pte on a Depth-Anything-V2 export,
concluding the conversion was broken, before finding the input was the difference.
The first three stages honour strides and agree; only the runtime disagrees.

Either honouring the strides or rejecting the mismatch would have cost me nothing
to debug. Rejecting seems in keeping with the existing check — the message just
needs to compare the input's layout against the method's expected layout rather
than accept channels-last unconditionally.

cc @mergennachin @kimishpatel @iseeyuan @larryliu0820 @JacobSzwejbka @lucylq

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.