Skip to content

[XNNPACK] Delegate GEMMs that take parts of one packed weight or bias - #25005

Draft
john-rocky wants to merge 1 commit into
pytorch:mainfrom
john-rocky:xnnpack-split-param-gemms
Draft

john-rocky wants to merge 1 commit into
pytorch:mainfrom
john-rocky:xnnpack-split-param-gemms

Conversation

@john-rocky

Copy link
Copy Markdown
Contributor

Summary

Follows #24352, which leaves a computation shared by several GEMMs on portable. Now GEMMs that take the parts of one split, such as the packed projections of nn.MultiheadAttention, are delegated together, and the split is folded inside their delegate.

nn.MultiheadAttention packs the weights of its query, key and value projections into one in_proj_weight and their biases into one in_proj_bias; when kdim or vdim differs from the embedding dim, the weights are separate params and only the biases are packed. Unless the query, key and value are one tensor, it splits each packed param with split or chunk and gives each part to its own linear. On main, the partitioner declines those linears because the split also feeds the other linears, so they run on the portable kernels in cross-attention, with different key and value tensors, with kdim and vdim, and in nn.TransformerDecoderLayer.

get_static_param_deps now also accepts a weight or bias computation in which a multi-output op gives its other outputs to other GEMMs, as long as they are of the same kind as the GEMM being checked, such as linear, and take their part in the same argument position. The config partitioner already merges partitions that share a node other than a constant, so these GEMMs become one partition, and FoldComputedParamsPass, which this PR does not change, folds the multi-output op into one constant per output. GEMMConfig.check_constraints accepts such a GEMM only together with the rest of its group. Every GEMM of the group must pass its own checks, and the merged partition of the group's GEMMs and their deps must have no node with too many dims and must pass validate_partition, the cycle check of fuse_as_graphmodule. The folded outputs together may be no larger than the params they free, as before.

Without these checks, lowering fails. If one GEMM of a group is accepted and another is not, the one left out takes the multi-output op's outputs from the delegate, and FoldComputedParamsPass raises. The same happens when a node with too many dims is in one GEMM's partition, because get_partition drops that GEMM after check_constraints has accepted it. A group with a cycle still becomes one partition, which fuse_as_graphmodule rejects.

#23154 folds constant cat and split chains before decomposition, in transform_for_pre_decomposition, and its test_split_forms_delegate_and_match_compiled_runtime covers three biases concatenated, split again and given to three linear or addmm calls. This PR folds such a chain inside the delegate of the GEMMs that use it, the way #24352 folds a computed weight or bias. With that test's ProjectionBiases module, all six cases lower to one delegate with no operator left outside it, and the outputs match eager.

Lowered with XnnpackPartitioner() on macOS arm64, embedding dim 16, 2 heads, batch_first=True, query (1, 5, 16), memory (1, 7, 16), default weights, biases drawn with torch.nn.init.normal_, main → this PR:

GEMMs left on portable delegate calls .pte bytes max abs diff vs eager
cross-attention 2 → 0 7 → 6 19,776 → 18,112 3.6e-07 → 2.4e-07
different key and value 3 → 0 4 → 3 18,240 → 16,832 2.4e-07 → 2.4e-07
kdim=12, vdim=10 3 → 0 4 → 3 17,088 → 15,680 4.8e-07 → 4.8e-07
nn.TransformerDecoderLayer, dim_feedforward=32 2 → 0 11 → 13 42,688 → 42,304 4.8e-07 → 4.8e-07
self-attention 0 → 0 6 → 6 17,600 → 17,600 1.9e-06 → 1.9e-06

The two extra delegate calls in nn.TransformerDecoderLayer come from its key and value projection, which now shares a delegate with the query projection and so runs after self-attention. The permute and clone after the key and value projection sit between unsqueeze, squeeze and select, which stay on portable, so they no longer fit into other delegates and get one each.

I have not measured latency.

This draft is for a look at the approach, as suggested on #24352. Suggested review order: get_static_param_deps, then GEMMConfig.check_constraints and the two new methods behind it, _check_gemms_sharing_deps and _get_gemms_sharing_deps.

Test plan

python -m pytest -v backends/xnnpack/test/passes/test_fold_computed_params.py

16 passed, 26 subtests passed. The new tests lower linears and addmms that take the parts of a packed weight or bias; the parts are made with split, with chunk, or with cat followed by chunk. They also lower nn.MultiheadAttention in cross-attention, in cross-attention with dynamic query and memory lengths, with different key and value tensors, with kdim and vdim, and in nn.TransformerDecoderLayer. In each of these, in_proj_bias is drawn at random, because it is zero by default. They check that no GEMM or split is left outside the delegates and that the runtime matches eager. For the linears and addmms they also check that a single delegate stores the parts, and not the packed param, as constants. A packed linear bias, and a packed linear weight with its bias, are also covered under static and dynamic quantization. These 14 subtests fail on main. The other new tests check what must not be delegated: a part also used by another op, a GEMM whose output passes through an op left on portable into the GEMM that takes the other part, parts that together are larger than the param, a GEMM of the group whose bias is computed with more dims than XNNPACK supports, and a conv1d that shares a split weight with a conv1d rejected for its dynamic shape. Each of the last four fails when its check is removed.

python -m pytest -q backends/xnnpack/test/passes backends/xnnpack/test/ops backends/xnnpack/test/serialization backends/xnnpack/test/quantizer backends/xnnpack/test/test_xnnpack_partitioner.py -o addopts="" --continue-on-collection-errors

Run on macOS arm64 without a source build. The Python sources of this PR and of main each ran on top of the executorch 1.5.1 wheel, which supplies the native runtime and the pybindings:

  • this PR: 70 failed, 521 passed, 1 skipped, 1 error, 213 subtests passed
  • main (7fa0b8a5b, the base of this PR): 69 failed, 518 passed, 1 skipped, 1 error, 195 subtests passed

In these runs, no failure is caused by this PR. Every test that fails on main also fails with this PR, which had one more failure. Most of the shared failures are Failed to execute method forward errors from the 1.5.1 runtime, and test_prelu.py is not collected because it imports ExecuTorchResult, which the 1.5.1 pybindings lack. The extra failure is TestLinear.test_qd8_per_channel_linear_parallel, which draws its inputs and batch size without a seed and compares with atol=0.05; the failing run had a max abs diff of 0.054. With seeds 0 to 29, main and this PR give the same max abs diff in every output comparison, and the same 4 seeds fail on both.

Tests that fail on both
  • test_mean_dim.py: 22 tests
  • test_io_dim_order.py: 7 tests
  • test_upsample_nearest2d.py: 6 tests
  • test_linear.py: test_linear_qd8_f16_per_channel_int4, test_linear_qd8_f16_per_token_weight_per_channel_group_int4, test_linear_qd8_f32_per_channel_int4
  • test_gelu.py: test_fp32_gelu_approximation_2_tanh, test_fp32_gelu_approximation_3_tanh
  • test_bmm.py: test_bf16_bmm
  • test_softmax.py: test_bf16_softmax
  • test_xnnpack_partitioner.py: test_enable_bf16_runs_an_attention_block
  • test_xnnpack_quantizer.py: test_gru
  • test_prelu.py: not collected

This PR was authored with Claude Code.

get_static_param_deps now also accepts a weight or bias computation whose
multi-output op, such as the split of nn.MultiheadAttention's packed
in_proj_weight, gives its other outputs to GEMMs of the same op as the same
argument. GEMMConfig.check_constraints accepts such a GEMM only together with
those GEMMs, and only if their merged partition has no node with too many dims
and creates no cycle. FoldComputedParamsPass folds the op inside the delegate
without changes.
@pytorch-bot

pytorch-bot Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⚠️ 16 Awaiting Approval

As of commit 203d0b3 with merge base 7fa0b8a (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.

@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 Nov 13, 2026
@john-rocky

Copy link
Copy Markdown
Contributor Author

@JakeStevens here is the draft for the packed nn.MultiheadAttention split; the approach is in get_static_param_deps and GEMMConfig.check_constraints.

@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.

This branch has not been deployed

No deployments
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.

1 participant