Skip to content

Serialize only a view constant's own bytes in XNNPACK and Vulkan - #24181

Merged
shoumikhin merged 1 commit into
pytorch:mainfrom
shoumikhin:view-constant-bytes
Oct 27, 2026
Merged

shoumikhin merged 1 commit into
pytorch:mainfrom
shoumikhin:view-constant-bytes

Conversation

@shoumikhin

@shoumikhin shoumikhin commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

What is wrong today

A constant tensor can be a view into a larger tensor. For example:

self.register_buffer("weight", torch.randn(3, 8, 8)[1])   # starts 64 floats into its storage
...
return F.linear(x, self.weight)

Constant folding can also leave a weight as a view, like one chunk of the packed in_proj_weight of nn.MultiheadAttention in cross-attention.

The XNNPACK and Vulkan serializers copy the bytes of the whole underlying storage, starting at its first byte. For a view that starts later in the storage, the delegate gets the wrong weights, and the .pte gives wrong results with no error. In the model above, the largest XNNPACK error was about 6 to 8. For a view that starts at the beginning, the values are right, but the file carries the whole parent.

XNNPACK per-channel scales are read the same way. They also skip the contiguous() step that weights get, so a transposed groupwise scale is written in the wrong order.

What this change does

  • Before reading a weight's bytes, both serializers now clone it if its storage is larger than the tensor. The ExecuTorch emitter already uses this rule for weights that are views (exir/tensor.py, exir/emit/_emitter.py).
  • XNNPACK per-channel scales are small, so they are now always copied into contiguous storage of their own.
  • The Vulkan path that serializes raw tensors (only tests use it today) now does the same.

What was tested

On macOS arm64. Each of these fails before this change and passes after:

  • test_fp32_linear_view_weight (XNNPACK): a linear whose weight is a view.
  • test_qd8_linear_transposed_scale (XNNPACK): a groupwise int4 linear with a transposed scale.
  • test_view_constant_serializes_only_its_own_bytes (Vulkan graph builder): checks the bytes of a view constant, no GPU needed.
  • test_serialize_vulkan_binary (existing): now also serializes a view and a transposed view.

The default backends/xnnpack/test run passes. It skips test_linear.py, so that file was run directly: all tests pass except test_linear_qd8_{f32,f16}_per_channel_int4, which fail the same way without this change. A per-channel scale replaced by an equal view was also checked by hand: off by about 4e4 before, exact after.

Not in this change

_tensor_to_bytes in exir/_serialize/_named_data_store.py has a similar problem: for a non-contiguous tensor, such as a channels_last slice of a bigger tensor, it writes the whole storage. XNNPACK and Vulkan pass it plain bytes, so this change does not depend on it. It is left for a separate fix.

cc @SS-JIA @manuelcandales @digantdesai @cbilgin

@shoumikhin shoumikhin added the release notes: xnnpack Changes to the XNNPack backend delegate label Oct 27, 2026
Copilot AI balanced review requested due to automatic review settings October 27, 2026 08:31
@pytorch-bot pytorch-bot Bot added the module: vulkan Issues related to the Vulkan delegate and code under backends/vulkan/ label Oct 27, 2026
@pytorch-bot

pytorch-bot Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⏳ No Failures, 2 Pending

As of commit 5610818 with merge base 5a66091 (image):
💚 Looks good so far! There are no failures yet. 💚

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 27, 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 review

@claude

claude Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 3m 39s —— View job


The fix is correct, but neither new XNNPACK regression test runs in OSS CI, and the same storage-from-byte-0 read is still in two other serialization paths.

Detailed review

PR Review: #24181

Summary

The PR clones view constants before their bytes are read, so the XNNPACK and Vulkan serializers write only the tensor's own bytes. It also always copies XNNPACK per-channel scales into contiguous storage. Three independent reviewers found no correctness bug in the changed serializer code. The problems are a CI coverage gap for the XNNPACK tests and two untouched paths with the same bug pattern.

Testing

  • backends/xnnpack/test/ops/test_linear.py:784, :920: neither XNNPACK test runs in OSS CI.
    • pytest.ini:15 has --ignore=backends/xnnpack/test/ops/test_linear.py, and no .ci/ or .github/ job runs that file another way. The internal ops/*.py glob in backends/xnnpack/test/BUCK may still pick it up.
    • If either XNNPACK fix is reverted, no OSS job fails.
    • The Vulkan test is not affected: it runs via .github/workflows/pull.yml:1795.
    • Suggested fix: move test_fp32_linear_view_weight and test_qd8_linear_transposed_scale into an XNNPACK test file that pytest collects. Alternatively, add a serializer-level byte check like the new Vulkan test, which also needs no runtime.
    • Fix this →

Code Quality

  • backends/vulkan/serialization/vulkan_graph_serialize.py:285-290: the raw torch.Tensor branch of serialize_constant_tensors still has the old pattern. It copies untyped_storage().nbytes() bytes from data_ptr(), with no view or contiguity check. VkGraphBuilder only appends (named_key, size) tuples or None (vulkan_graph_builder.py:190,194), so the builder never reaches this branch today. Tests such as test_serialization.py still use it. Either apply the same guard there or remove the branch, so the bug cannot come back through it.
  • exir/_serialize/_named_data_store.py:27-30: same bug class in shared code, out of scope for this PR. For a non-contiguous tensor, _tensor_to_bytes falls back to bytes(tensor.untyped_storage()), which is the whole storage from byte 0. A channels_last slice of a larger tensor would be written with the wrong bytes and the wrong length. XNNPACK and Vulkan pass bytes here, so this PR is not affected. A follow-up issue, or a line in the PR description, would keep this from getting lost.

Recommendation

Approve

Nothing in the serializer changes blocks merging. Ideally, move the XNNPACK tests into a file that CI collects before merging, so the fix has regression coverage.

I could only read code in this environment: running Python needed approval that wasn't granted. So I didn't run the tests myself, and I'm relying on the PR description that each new test fails without the fix.

A constant tensor can be a view into a larger tensor, for example a
buffer registered as `packed[8:16]`, or a slice that constant folding
leaves as a view. The XNNPACK and Vulkan serializers copied the bytes
of the whole underlying storage starting at its first byte. For a view
that starts later in the storage, the delegate then got the wrong
weights, and the .pte gave wrong results with no error:

    self.register_buffer("weight", torch.randn(3, 8, 8)[1])
    F.linear(x, self.weight)   # XNNPACK result was far from eager

Both serializers now clone a weight whose storage is larger than the
tensor before reading its bytes. This is the same rule the ExecuTorch
emitter already uses for weights that are views.

XNNPACK per-channel scales had the same problem, and also skipped the
contiguous() step that weights get, so a transposed groupwise scale was
written in the wrong order. Scales are small, so they are now always
copied into contiguous storage of their own before serialization.
The Vulkan path that serializes raw tensors, which only tests use
today, now does the same.

Tested on macOS arm64, each new test failing before this change and
passing after:
- an XNNPACK linear whose weight is a view (output mismatch before)
- an XNNPACK groupwise int4 linear with a transposed scale (output
  mismatch before)
- a Vulkan graph builder check of the serialized bytes of a view
  constant
- the existing Vulkan serialization test, which now also serializes a
  view and a transposed view
The default backends/xnnpack/test suite passes. Run on its own,
test_linear.py passes except test_linear_qd8_{f16,f32}_per_channel_int4,
which fail the same way without this change. A per-channel scale
replaced by an equal view in an exported program was also checked by
hand: the result differed by 4e4 before, and matches after.
Copilot AI balanced review requested due to automatic review settings October 27, 2026 17:02

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

Thanks for the review. Here is what I did with each point.

  1. XNNPACK tests not run by open-source CI: you are right, no open-source job runs them today. I kept them in test_linear.py with the other XNNPACK linear op tests. pytest.ini has skipped that file since late 2024. Its comment says it has failing tests, and two still fail. The BUCK target test_xnnpack_ops still includes it. A byte check in another file would test the same fix twice, so I did not add one. Bringing test_linear.py back into the default run is a separate change.

  2. Raw tensor path in serialize_constant_tensors: fixed. It now copies each tensor into contiguous storage of its own first. The existing test_serialize_vulkan_binary now also serializes a view and a transposed view, and it fails without the fix.

  3. _tensor_to_bytes in the named data store: agreed, it is the same kind of bug. I checked it: it writes the whole parent storage, so a channels_last slice of a bigger tensor gets the wrong length, and the wrong bytes if the slice does not start at the beginning. This change does not use it, so I listed it in the description as a follow-up.

@shoumikhin
shoumikhin merged commit b3bd402 into pytorch:main Oct 27, 2026
219 checks passed
@shoumikhin

Copy link
Copy Markdown
Contributor Author

@pytorchbot cherry-pick --onto release/1.6 -c critical

@pytorchbot

Copy link
Copy Markdown
Collaborator

Cherry picking #24181

The cherry pick PR is at #24208 and it is recommended to link a critical cherry pick PR with an issue.

Details for Dev Infra team Raised by workflow job

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. module: vulkan Issues related to the Vulkan delegate and code under backends/vulkan/ release notes: xnnpack Changes to the XNNPack backend delegate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants