Skip to content

XNNPACK: fix enable_bf16 load failures and BatchNorm folding into a 3-D linear output - #24477

Merged
JacobSzwejbka merged 4 commits into
release/1.6from
cherry-pick-24464-by-pytorch_bot_bot_
Nov 3, 2026
Merged

JacobSzwejbka merged 4 commits into
release/1.6from
cherry-pick-24464-by-pytorch_bot_bot_

Conversation

@pytorchbot

Copy link
Copy Markdown
Collaborator

What is wrong today

XnnpackPartitioner(enable_bf16=True) lets XNNPACK run bf16 nodes. Without it, bf16 matrix multiplies run on the single-threaded portable kernels, which is very slow. With it, a program can fail to load:

  1. The partitioner hands XNNPACK bf16 nodes that XNNPACK cannot define in bf16: softmax, mean, average and max pooling, bilinear upsampling, and batch norm that does not fold into a linear. Loading fails with xnn_status_invalid_parameter.
  2. Batch matrix multiply (bmm) is defined with a bf16 output, but XNNPACK only multiplies bf16 matrices into an fp32 result. Loading fails with Failed to create BMM node.

A bf16 attention block written with matmul and softmax hits both.

What this change does

  • A config can now say it does not support bf16 (supports_bf16 = False), so its bf16 nodes stay on the portable kernels and the partitioner logs why. Softmax, mean, both poolings, bilinear upsampling and convolution set it. The default is True: XNNPACK rewrites most bf16 ops to fp32 internally, and at the pinned commit only these reject bf16 when defined. A bf16 run of the other float ops found no other failure.
  • A bf16 batch norm is kept in XNNPACK only when FuseBatchNormPass.can_fuse says it folds into the linear before it.
  • can_fuse now also requires the batch norm to be the only user of the linear output, and the linear output to be 2-D. Without these checks, a bf16 batch norm could be kept but left unfused, so loading failed, and a batch norm after a 3-D linear output was folded along the wrong dim, which gave wrong results in fp32 and bf16.
  • A bf16 bmm now writes an fp32 result and converts it to bf16, as the linear layer already did.

With this change, the SmolVLA robot policy (a vision-language-action model) in bf16 with enable_bf16=True loads, and then runs one action chunk in about 11 seconds on 16 x86 cores, instead of about 9 minutes with the default partitioner.

Tests

  • test_enable_bf16_leaves_ops_xnnpack_cannot_define_in_bf16: each op, and a convolution followed by a batch norm, is delegated in fp32 but not in bf16, and the bf16 program matches eager.
  • test_enable_bf16_fuses_batch_norm_into_a_linear: a bf16 linear followed by BatchNorm1d runs fully in XNNPACK. When the linear output is also used elsewhere, the batch norm stays out.
  • test_enable_bf16_runs_an_attention_block: the linear layers and both bmms run in XNNPACK, the softmax stays outside.
  • test_bf16_bmm in test_bmm.py, and test_fp32_linear_batch_norm_on_3d_output in test_batch_norm_fusion.py.

All of these fail before this change, except the fused batch norm case, which guards the fusion that already worked. Ran the XNNPACK partitioner, op and pass tests on Linux x86_64. The only failures, in the per-channel int4 linear tests, also happen without this change.

shoumikhin and others added 3 commits November 2, 2026 16:51
…out (#24380)

## What is wrong today

XNNPACK supports tensors with at most 6 dimensions, but the XNNPACK
partitioner, which decides which ops run on XNNPACK, does not check
this. A permute or another op on a 7-D or 8-D tensor then lowers fine
but fails to load:

```
Failed to define tensor 0 with code: xnn_status_unsupported_parameter
```

Some vision models hit this in a "patchify" step that cuts an image into
patches:

```python
y = x.reshape(2, 6, 8, 2, 16, 8, 2, 16).permute(0, 2, 5, 3, 6, 1, 4, 7)
y = y.reshape(2, 64, -1) * 2
```

With the 3-D convolution kernel from #24385, a statically quantized
`Conv3d` is worse: it fails to load or, without a fused ReLU, returns
one wrong value repeated everywhere, with no error. XNNPACK has no 3-D
convolution, so the partitioner leaves the conv out, but only after
marking the quantize and dequantize nodes around it as folded into it.
Those nodes then land in delegates that skip them, so nothing writes the
outputs. A statically quantized `Conv2d` or `Linear` whose output is not
quantized hits the same bug, even without #24385.

## What this change does

The partitioner now leaves out any op with an input or output above 6
dimensions, and it checks every node that a partition takes. A float
linear on such an input still keeps its 2-D matrix multiply on XNNPACK.

Quantize and dequantize nodes are now marked as folded only once the op
they belong to is really partitioned, so a rejected op keeps them as
separate ops.

`force_non_static_weights_for_f32_linear` no longer changes how a
quantized linear is partitioned. With it, a quantized linear decomposed
into a matrix multiply (for example with `to_backend`) used to lose its
quantize and dequantize nodes: a static one failed to lower or load, and
a dynamic one ran as a float matrix multiply. Now it is partitioned as
without the flag.

The XNNPACK quantizer now leaves 3-D convolutions in float. The model
file gets larger, but there are no extra quantize and dequantize steps
and no quantization error. They run on the CPU kernels, which support
3-D convolution once #24385 lands. Without it, such a model fails with
"Expect input tensor to be 3-D or 4-D".

Still not covered: a linear on such an input, quantized with torchao's
affine ops. Those ops have no portable kernels, so the model now fails
in `to_executorch()` instead of at load. And a linear on such an input
quantized with the XNNPACK quantizer runs outside XNNPACK, because its
quantized input has too many dims.

## Testing

- `test_permute.py` checks which ops are delegated for 6-D and 7-D
permutes, and for a linear on an 8-D input: float (also with a ReLU),
static and dynamic quantization, each in both partitioner modes.
- New tests cover a statically quantized Conv2d and Linear with a float
output, a statically and a dynamically quantized linear lowered through
`to_backend` with `force_non_static_weights_for_f32_linear`, and 3-D
convolutions that the quantizer leaves unquantized.
- The per-channel transposed conv test now also runs the convs that
XNNPACK rejects. Before this change they returned all zeros.
- Each new test fails without the part of the change it covers.
- On macOS arm64, the patchify example and other 7-D and 8-D cases load
and match eager PyTorch. A Conv3d model quantized with the XNNPACK
quantizer matches eager PyTorch with #24385, and fails without it.

(cherry picked from commit 1549787)
…-D linear output (#24464)

### What is wrong today

`XnnpackPartitioner(enable_bf16=True)` lets XNNPACK run bf16 nodes.
Without it, bf16 matrix multiplies run on the single-threaded portable
kernels, which is very slow. With it, a program can fail to load:

1. The partitioner hands XNNPACK bf16 nodes that XNNPACK cannot define
in bf16: softmax, mean, average and max pooling, bilinear upsampling,
and batch norm that does not fold into a linear. Loading fails with
`xnn_status_invalid_parameter`.
2. Batch matrix multiply (`bmm`) is defined with a bf16 output, but
XNNPACK only multiplies bf16 matrices into an fp32 result. Loading fails
with `Failed to create BMM node`.

A bf16 attention block written with `matmul` and `softmax` hits both.

### What this change does

- A config can now say it does not support bf16 (`supports_bf16 =
False`), so its bf16 nodes stay on the portable kernels and the
partitioner logs why. Softmax, mean, both poolings, bilinear upsampling
and convolution set it. The default is `True`: XNNPACK rewrites most
bf16 ops to fp32 internally, and at the pinned commit only these reject
bf16 when defined. A bf16 run of the other float ops found no other
failure.
- A bf16 batch norm is kept in XNNPACK only when
`FuseBatchNormPass.can_fuse` says it folds into the linear before it.
- `can_fuse` now also requires the batch norm to be the only user of the
linear output, and the linear output to be 2-D. Without these checks, a
bf16 batch norm could be kept but left unfused, so loading failed, and a
batch norm after a 3-D linear output was folded along the wrong dim,
which gave wrong results in fp32 and bf16.
- A bf16 `bmm` now writes an fp32 result and converts it to bf16, as the
linear layer already did.

With this change, the SmolVLA robot policy (a vision-language-action
model) in bf16 with `enable_bf16=True` loads, and then runs one action
chunk in about 11 seconds on 16 x86 cores, instead of about 9 minutes
with the default partitioner.

### Tests

- `test_enable_bf16_leaves_ops_xnnpack_cannot_define_in_bf16`: each op,
and a convolution followed by a batch norm, is delegated in fp32 but not
in bf16, and the bf16 program matches eager.
- `test_enable_bf16_fuses_batch_norm_into_a_linear`: a bf16 linear
followed by `BatchNorm1d` runs fully in XNNPACK. When the linear output
is also used elsewhere, the batch norm stays out.
- `test_enable_bf16_runs_an_attention_block`: the linear layers and both
`bmm`s run in XNNPACK, the softmax stays outside.
- `test_bf16_bmm` in `test_bmm.py`, and
`test_fp32_linear_batch_norm_on_3d_output` in
`test_batch_norm_fusion.py`.

All of these fail before this change, except the fused batch norm case,
which guards the fusion that already worked. Ran the XNNPACK
partitioner, op and pass tests on Linux x86_64. The only failures, in
the per-channel int4 linear tests, also happen without this change.

(cherry picked from commit 7bf0303)
@pytorch-bot

pytorch-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⏳ 3 Pending, 1 Unrelated Failure

As of commit 21014eb with merge base 4db778e (image):

BROKEN TRUNK - The following job failed but were present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

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 3, 2026
Base automatically changed from cherry-pick-24380-by-pytorch_bot_bot_ to release/1.6 November 3, 2026 04:02
@JacobSzwejbka
JacobSzwejbka merged commit 12cc460 into release/1.6 Nov 3, 2026
210 of 211 checks passed
@JacobSzwejbka
JacobSzwejbka deleted the cherry-pick-24464-by-pytorch_bot_bot_ branch November 3, 2026 06:50

This branch was successfully deployed

1 active deployment
cadence — 21014eb6 Deployed Nov 3, 2026 by shoumikhin via hifi5s-op-test / hifi5s #36159
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.

3 participants