Metal backend: ops read a view with a storage offset before the GPU has written it (silently wrong results)
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 35/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Domain
- backend, machine-learning
Research direction
Start with backends/apple/metal/runtime/shims/memory.cpp and trace a_reinterpret_tensor handling for non-zero storage offsets and packed views. Run backends/apple/metal/tests/run_metal_test.sh --build, then the unittest command for TestMetalBackendModules -k linear_chunk. Done means both named tests produce correct outputs for float32 and bfloat16 rather than consuming zeros.
Written by the indexing model from the issue text.
Description
🐛 Describe the bug
On the Metal backend, an op that consumes a view starting partway into another op's output reads that memory before the GPU has written it. The result is silently wrong (zeros in the minimal cases below); nothing fails. chunk, split and unbind all produce such views, so this shows up in real models: the YOLO family exported for Metal runs but returns box coordinates that are off by hundreds.
Repro. Two modules for MODULE_REGISTRY in backends/apple/metal/tests/test_modules.py. Both feed the second chunk of a linear's output into another linear:
class LinearChunkLastDim(nn.Module):
def __init__(self):
super().__init__()
self.linear1 = nn.Linear(7, 16, bias=False)
self.linear2 = nn.Linear(8, 5, bias=False)
def forward(self, x):
_, second = self.linear1(x).chunk(2, dim=-1)
return self.linear2(second)
class LinearChunkFirstDim(nn.Module):
def __init__(self):
super().__init__()
self.linear1 = nn.Linear(7, 16, bias=False)
self.linear2 = nn.Linear(16, 5, bias=False)
def forward(self, x):
_, second = self.linear1(x).chunk(2, dim=0)
return self.linear2(second)
with "input_shapes": [(12, 7)].
backends/apple/metal/tests/run_metal_test.sh --build
python -m unittest backends.apple.metal.tests.test_modules.TestMetalBackendModules -k linear_chunk
linear_chunk_last_dim (float32): Output mismatch - max_atol=0.24710503220558167, max_rtol=1.0
linear_chunk_first_dim (float32): Output mismatch - max_atol=0.857249915599823, max_rtol=1.0
max_rtol=1.0 in all four cases (float32 and bfloat16): the consumer saw zeros. Using the first chunk instead (offset 0) gives the right answer.
Cause. Inductor hands the view to the second mm as reinterpret_tensor_wrapper(buf0, ..., storage_offset), and aoti_torch__reinterpret_tensor in backends/apple/metal/runtime/shims/memory.cpp has two paths, both of which miss the pending write to buf0:
-
Last-dim chunk: sizes
[12, 8], strides[16, 1], offset 8. Not packed, somaterialize_packedcopies it on the CPU. It does wait for the GPU first, but only underif (metal_is_device_pointer(src)) { stream->synchronize(SyncType::COMMIT_AND_WAIT); }and
srcisdata_ptr + storage_offset * element_size. Only a buffer's base address is a key inptr_to_mtl_buffer, so for any non-zero offset the check is false, the wait is skipped, and the copy reads memory the firstmmhas only been encoded to write. -
First-dim chunk: sizes
[6, 16], strides[16, 1], offset 96. Packed, so the view gets its own buffer throughmetal_buffer_nocopy(adjusted_data, ...). That is a secondMTLBufferover the parent's memory, and Metal tracks hazards per buffer object, so nothing orders the work reading the alias after the pending work writing the parent. Waiting for the stream before creating the alias makes the result correct, which is what points at ordering rather than addressing (a no-copy buffer over the unaligned pointer does read the right bytes once the memory is settled).
I have a fix with these two tests and will open a PR.
Versions
ExecuTorch: main @ 11120c8dff (also reproduced with the 1.5.0 release: PyPI wheel for export, runtime built from the v1.5.0 tag)
PyTorch version: 2.14.0
torchao: built from source with TORCHAO_BUILD_EXPERIMENTAL_MPS=1
OS: macOS 27.0 (arm64), Apple M2 Pro
Clang version: 21.0.0 (clang-2100.1.1.101)
CMake version: 4.4.3
Python version: 3.10.11
- Dominant language
- Python
- Stars
- 5k
- Forks
- 1.2k
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 559
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from pytorch/executorch
-
enhancement triaged
Difficulty 2/5 Half a day Newbie friendliness 68/100
pytorch/executorch#21640 ·
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
pytorch/executorch#22955 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 35/100
pytorch/executorch#22953 ·
-
pytorch/executorch#22845 · 1 assignee ·
-
bug module: vulkan triaged
pytorch/executorch#22775 · 3 comments · 1 assignee ·
All issues in pytorch/executorch
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
zostera/django-bootstrap4#894 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
use-agent-os/agent-os#3276 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
zephyrproject-rtos/zephyr#119726 ·
-
area/auth bug comp/agent P3 platform/discord type/security
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
NousResearch/hermes-agent#117848 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
zilliztech/memsearch#759 ·