Repository navigation
Commit b3e2457
authored
Lower mean.dim beyond 4D pooling as an XNNPACK static mean (pytorch#24741)
## Ulterior Motive
Run RMSNorm in LeRobot policies on XNNPACK (pytorch#24667), where `mean.dim`
fails to partition with "only supports averaging 4D tensors, got tensor
of rank 3".
## Rationale
**What**: Add an `XNNStaticMean` node backed by
`xnn_define_static_reduce(xnn_reduce_mean)`, and lower every `mean.dim`
that is not Global Average Pooling to it: ranks 1 to 6, any dims, and
either `keepdim`.
**Why**: XNNPACK only lowered a 4D mean over dims [2, 3] with
`keepdim=True`. RMSNorm averages the last dim of a 3-D tensor, so its
mean stayed on the portable kernel and split the delegate around every
norm.
## Details
Review order: the serialization schema and `XNNCompiler.cpp`, then
`op_mean_dim.py` and `is_global_avg_pool_mean`, then
`ChannelsLastTaggedReshapePass`, then `MeanDimConfig`, then the tests.
- The 4D, dims [2, 3], `keepdim=True` case still lowers to Global
Average Pooling on NHWC. For those programs the `.pte` is byte-identical
to main (fp32, fp16, qs8, conv-bn-hardtanh-mean, and a CNN ending in
`AdaptiveAvgPool2d(1)`), and the tests check which XNNPACK node each
mean serializes to and how many transposes surround it.
- Every other `mean.dim` reduces over NCHW dims, so
`ChannelsLastTaggedReshapePass` gives it an NCHW input, and the visitor
refuses an NHWC one as an internal layout error. After a convolution
that adds layout copies inside the one delegate. For conv then `mean(1,
keepdim=True)` then conv, the program has 4 transposes, as on main. For
`y - y.mean(1, keepdim=True)` after a convolution (the centering in
ConvNeXt's LayerNorm2d), the subtraction stays NHWC, so its full-size
result is copied back to NCHW at the output: 4 transposes in one
delegate, where main has 2 in two delegates around a portable mean. At
96 channels and 56x56 that is 0.75x at 1 thread, 0.85x at 4 and 1.12x at
16, and conv, mean, conv is 0.54x, 0.70x and 0.90x. Remapping the
reduction axes to read NHWC directly would remove these copies, also for
heads like `x.mean([2, 3])`; that is left for a follow-up.
- `is_global_avg_pool_mean` takes the rank from the mean's own output,
which with `keepdim=True` equals the input's, because inside
`ChannelsLastTaggedReshapePass` the mean's input can be a newly inserted
node without `"val"`.
- The node uses a shared `_XNNStaticReduce` table, so other XNNPACK
reductions can reuse it and `defineGenericReduceNode`. It is appended to
`XNodeUnion`, so existing node IDs do not change. Practically every
re-exported XNNPACK LLM program contains it, because RMSNorm
(`examples/models/llama/norm.py`) and QK-norm lower to it, and those
programs fail to load on runtimes built before this change, including
released prebuilt runtimes, with `Unhandled node type` in the log. The
release note should say so.
- `ChannelsLastTaggedReshapePass` now reads the layout of a quantized
graph input from the input's own tag. It used to test `meta["val"][0]`,
a 3-D slice that is never channels_last, so a channels_last input behind
a quantize and dequantize read as NCHW. A mean over NCHW dims of such an
input then reduced the wrong axes, and on main a convolution of it got a
transpose that fails at runtime. Both now lower correctly.
- An empty dim list averages over every dim, as in PyTorch.
- Latency against main, branch time over main time (below 1 is faster),
each a median over at least 1000 timed runs per program, alternating
main and branch programs on one runtime, on an Apple M-series CPU (12
performance + 4 efficiency cores) with the default 16 threads: RMSNorm
1x128x2048 0.48x, 3-D last-dim mean 0.45x, RMSNorm then Linear 0.77x, 4
pre-norm residual MLP blocks 0.99x for decode (1 token) and 0.80x for
prefill (64 tokens), conv then mean over channels 0.88x.
- XNNPACK splits a reduction with more than one output into threadpool
tasks, so a small mean with several outputs can cost more in XNNPACK
than on the portable kernel when the pool has many threads. For RMSNorm
on 10 shapes from 1x1x128 to 1x32x2048, every shape is 0.43x to 0.97x at
1 thread. At 4 threads the shapes under 8K elements with several outputs
are 1.19x to 1.55x (1.5 to 2.7 us more), and the rest 0.44x to 0.93x. At
16 threads those small shapes are 3.7x to 5.7x (21 to 23 us more), and
the rest 0.70x to 0.98x. Inside a model with neighbouring delegated ops
the cost can remain: decode QK-norm between two linears, 8 heads of 64,
goes from 11.8 us to 39.1 us at 16 threads, and 16 heads of 128 from 222
us to 241 us. A mean alone in its delegate pays the same wake-up (a bare
1x128x128 mean is 3.26x at 16 threads and 1.14x at 4), as does any op
lowered alone, such as `add` (2.95x at 1x128x128 and 16 threads on
main). These means are lowered like every other op the XNNPACK
partitioner takes, rather than gated by size, which would be tuned to
one thread count and treat static and dynamic exports differently. End
to end, Qwen3-0.6B with 8da4w quantization (113 RMSNorm and QK-norm
means, 368 delegates on main and 283 here) decodes, after over before in
`executor_runner` with the LLM custom ops: on the same Apple M-series
CPU 0.98x at 1 thread and 1.11x to 1.12x at the default 16 threads (97
to 87 tokens/s); on an iPhone 17 1.00x at 1 thread and 1.05x at the
runner's default (101 to 96 tokens/s); on a Pixel 10 0.98x and 1.00x.
The cost is the multi-head QK-norm means. Running small multi-output
reductions without waking the pool belongs in XNNPACK and is left for a
follow-up.
- The LoRA CI scripts check the start of the 8da4w-quantized Qwen3 base
model's greedy output with a regex. With RMSNorm and QK-norm now in
XNNPACK, its first token can tip from `:\n` to `:` (`assistant: me`
instead of `assistant:\n<think>`): unquantized, main and this branch
generate the same tokens with logits within 1e-4, but 8da4w re-rounds
activations and the two candidates are within about 0.5 logits. The
regex accepts exactly those two continuations, and the scripts print it
when it does not match.
- Tests cover 1-D to 6-D inputs, all dims, `keepdim=False`, fp16 and qs8
over the innermost and a middle dim, dynamic kept and reduced dims over
5 random shapes, RMSNorm, fp32 and qs8 convolutions followed by a mean,
a mean over channels whose result meets the convolution output or feeds
another convolution (as in ConvNeXt's LayerNorm2d), qs8 channels_last
inputs to a mean and to a convolution, the serialized XNNPACK nodes
(moved `xnnpack_node_types` from the layout pass tests to the XNNPACK
`Tester`), and the layout copies the pass inserts.
Test plan:
```
python -m pytest backends/xnnpack/test/ops/test_mean_dim.py backends/xnnpack/test/passes/test_channels_last_tagged_reshape.py
python -m pytest backends/xnnpack/test/ops backends/xnnpack/test/passes backends/xnnpack/test/test_xnnpack_partitioner.py backends/xnnpack/test/serialization backends/xnnpack/test/quantizer
```
Authored with AI using Claude Code.1 parent dd38e81 commit b3e2457
15 files changed
Lines changed: 621 additions & 210 deletions
File tree
- .ci/scripts
- backends/xnnpack
- _passes
- operators
- partition/config
- runtime
- serialization
- test
- ops
- passes
- tester
- utils
- docs/source/backends/xnnpack
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
49 | 52 | | |
50 | | - | |
51 | | - | |
| 53 | + | |
| 54 | + | |
52 | 55 | | |
53 | 56 | | |
54 | 57 | | |
55 | | - | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
56 | 61 | | |
57 | 62 | | |
58 | 63 | | |
| |||
174 | 179 | | |
175 | 180 | | |
176 | 181 | | |
177 | | - | |
178 | | - | |
179 | | - | |
180 | 182 | | |
181 | 183 | | |
182 | 184 | | |
| |||
238 | 240 | | |
239 | 241 | | |
240 | 242 | | |
241 | | - | |
242 | 243 | | |
243 | 244 | | |
244 | 245 | | |
245 | | - | |
246 | 246 | | |
247 | 247 | | |
248 | 248 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
49 | 52 | | |
50 | | - | |
51 | | - | |
| 53 | + | |
| 54 | + | |
52 | 55 | | |
53 | 56 | | |
54 | 57 | | |
55 | | - | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
56 | 61 | | |
57 | 62 | | |
58 | 63 | | |
| |||
107 | 112 | | |
108 | 113 | | |
109 | 114 | | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | 115 | | |
115 | 116 | | |
116 | 117 | | |
| |||
151 | 152 | | |
152 | 153 | | |
153 | 154 | | |
154 | | - | |
155 | 155 | | |
156 | 156 | | |
157 | 157 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| |||
69 | 70 | | |
70 | 71 | | |
71 | 72 | | |
72 | | - | |
73 | 73 | | |
74 | 74 | | |
75 | 75 | | |
| |||
137 | 137 | | |
138 | 138 | | |
139 | 139 | | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
148 | 143 | | |
149 | 144 | | |
150 | 145 | | |
151 | 146 | | |
152 | 147 | | |
153 | | - | |
154 | | - | |
155 | | - | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | | - | |
160 | | - | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | | - | |
165 | | - | |
166 | 148 | | |
167 | 149 | | |
168 | 150 | | |
| |||
316 | 298 | | |
317 | 299 | | |
318 | 300 | | |
| 301 | + | |
| 302 | + | |
319 | 303 | | |
320 | 304 | | |
321 | 305 | | |
322 | 306 | | |
323 | 307 | | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
324 | 312 | | |
325 | 313 | | |
326 | 314 | | |
| |||
546 | 534 | | |
547 | 535 | | |
548 | 536 | | |
549 | | - | |
550 | | - | |
551 | | - | |
552 | | - | |
553 | | - | |
554 | | - | |
555 | 537 | | |
556 | 538 | | |
557 | 539 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
11 | 14 | | |
12 | 15 | | |
13 | 16 | | |
| |||
17 | 20 | | |
18 | 21 | | |
19 | 22 | | |
| 23 | + | |
20 | 24 | | |
21 | 25 | | |
22 | | - | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
23 | 31 | | |
24 | 32 | | |
25 | 33 | | |
26 | 34 | | |
27 | 35 | | |
28 | 36 | | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
33 | 41 | | |
34 | 42 | | |
35 | 43 | | |
| |||
44 | 52 | | |
45 | 53 | | |
46 | 54 | | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | | - | |
56 | | - | |
57 | | - | |
58 | | - | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | 55 | | |
64 | 56 | | |
65 | 57 | | |
66 | 58 | | |
67 | 59 | | |
68 | | - | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | | - | |
76 | | - | |
77 | | - | |
78 | | - | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
79 | 63 | | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
80 | 67 | | |
81 | | - | |
82 | | - | |
| 68 | + | |
| 69 | + | |
83 | 70 | | |
84 | | - | |
85 | | - | |
86 | | - | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
87 | 90 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
715 | 715 | | |
716 | 716 | | |
717 | 717 | | |
718 | | - | |
719 | | - | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
720 | 721 | | |
721 | 722 | | |
722 | 723 | | |
723 | 724 | | |
724 | | - | |
725 | | - | |
726 | | - | |
727 | | - | |
728 | | - | |
729 | | - | |
730 | | - | |
731 | | - | |
732 | | - | |
733 | | - | |
| 725 | + | |
734 | 726 | | |
735 | 727 | | |
736 | 728 | | |
737 | 729 | | |
738 | | - | |
| 730 | + | |
739 | 731 | | |
740 | | - | |
| 732 | + | |
741 | 733 | | |
742 | 734 | | |
743 | 735 | | |
744 | 736 | | |
745 | | - | |
746 | | - | |
747 | | - | |
748 | | - | |
749 | | - | |
750 | | - | |
751 | | - | |
752 | | - | |
753 | | - | |
754 | | - | |
755 | | - | |
756 | 737 | | |
757 | 738 | | |
758 | 739 | | |
| |||
0 commit comments