Fold parameter-only subgraphs before XNNPACK partitioning - #22391
Fold parameter-only subgraphs before XNNPACK partitioning#22391john-rocky wants to merge 2 commits into
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22391
Note: Links to docs will display an error until the docs builds have been completed.
|
This PR needs a
|
|
a few thoughts: (1) seems there is a bug in the underlying pass for non-determinant ops: (2) I don't really like adding a flag to to_edge_transform_and_lower for a pass. maybe better to enable it only for xnnpack in the pre annotation transform. else there may be a better place to hook this in. thoughts maybe @JacobSzwejbka ? |
17e2c84 to
b65d16f
Compare
## Summary `constant_prop_pass` folds every `call_function` node whose arguments are all constants. Ops that draw from the RNG take only sizes as arguments, so they qualify: a model returning `x + torch.rand(4)` came out of the pass with the draw frozen into `_prop_tensor_constant0`, and returned the same value on every call. @JakeStevens spotted this while reviewing #22391 (repro there). The pass already runs in the Qualcomm and Samsung backends and in `quant_fusion_pass`, so the fix stands on its own. Skip nodes that `torch.fx.Node.is_impure()` reports as impure. That covers ops tagged `nondeterministic_seeded` (`rand`, `randn`, `bernoulli`, `dropout`, ...), mutable schemas and side-effectful functions, and is the same check `eliminate_dead_code` uses to decide what it must keep. ## Test plan New `test_constant_prop_pass_skips_nondeterministic_ops` in `exir/tests/test_passes.py`: after the pass one `aten.rand` node remains, no constant was added, and two calls give different outputs. It fails on main with `0 != 1`. `python -m unittest executorch.exir.tests.test_passes -k constant_prop`: 15 tests pass (torch 2.13.0, macOS arm64).
|
some feedback:
after these fixes please make sure to run linter as that is failing already |
The XNNPACK partitioner configs require a static weight, so a convolution or a linear whose weight is computed from parameters, such as anything under torch.nn.utils.parametrizations.weight_norm, was declined and left to the portable kernels together with the weight computation. On wav2vec2-large's positional convolution that is 3.9 s instead of 4.7 ms for the module alone (pytorch#22078). Override Partitioner.transform_for_pre_decomposition in XnnpackPartitioner to run constant_prop_pass on the ATen program. The skip set mirrors the pass's edge-level default: the factory ops that decompose to aten.full, so a scalar fill does not become a stored tensor, and the quantization primitives, so the Q/DQ chain convert_pt2e leaves on a weight stays in place.
…deps
The ATen program handed to transform_for_pre_decomposition is not
functionalized: a KV-cache update is still an in-place copy_, index_put_
or custom op on the buffer, and the graph signature lists no mutated
buffers. constant_prop_pass reads mutation from the signature, so it took
such a buffer for a constant and folded the view that is written to, and
run_decompositions then failed on the aliasing with "expected compiled_fn
to be GraphModule". This is what broke the Voxtral realtime export in CI.
Functionalize first with run_decompositions({}), the call
to_edge_transform_and_lower makes right after the hook anyway.
Also add aten.new_full, new_ones and new_zeros to the factory skip set,
since they decompose to aten.full as well, add the quantizer and torchao
deps to the test target, and cover both with regression tests.
b65d16f to
28837bf
Compare
|
Thanks for the review, @JakeStevens. All three points are in, plus one more fix the CI run turned up. The branch is rebased on main now that #22418 landed: the hook (7dec35b) and the review fixes (28837bf). Your three points. The Voxtral job was this PR, not the trunk. The ATen program at this hook is not functionalized yet: the KV-cache update is still an in-place op on the buffer ( On the hook location, from your first note: Of the other red jobs on the previous run: the three Thanks again for the careful look. |
Summary
Fixes #22078.
The XNNPACK partitioner configs require a static weight (
is_param_nodeinpartition/config/gemm_configs.py), so a convolution or a linear whose weight is computed from parameters, which is anything undertorch.nn.utils.parametrizesuch asweight_normandspectral_norm, is declined and left to the portable kernels together with the weight computation. Nothing warns:WhyNoPartitionlogs at DEBUG, and the model simply runs slow.This overrides
Partitioner.transform_for_pre_decompositioninXnnpackPartitionerto runconstant_prop_passon the ATen program, so the fold is XNNPACK-scoped and nothing changes into_edge_transform_and_lower's signature.Two things the hook has to get right at this stage of the pipeline:
copy_,index_put_or custom op on the buffer, and the graph signature lists no mutated buffers.constant_prop_passreads mutation from the signature, so on its own it takes such a buffer for a constant and folds the view that is written to;run_decompositionsthen fails on the aliasing (expected compiled_fn to be GraphModule), which is what the Voxtral realtime job hit on the previous revision. The hook functionalizes first withrun_decompositions({}), the callto_edge_transform_and_lowermakes right after it anyway. Cost on a 12-layer, 134M-parameter decoder with 24 cache writes: 0.6 s.aten.full(full,new_full,ones,new_ones,zeros,new_zeros) and toaten.full_like(full_like,ones_like,zeros_like), so a scalar fill does not become a stored tensor, for the same reason the pass skipsfullat the edge level. And everyquantized_decomposed/torchaoop in the graph, so the Q/DQ chainconvert_pt2eorquantize_leaves on a weight stays in place.Earlier revisions: a
constant_propflag onto_edge_transform_and_lower(reworked after review), then the hook without the functionalize step (broke the Voxtral export). The impure-op fix the first review found in the pass itself landed as #22418.Measurements
wav2vec2-large's positional convolution,
weight_norm(Conv1d(1024, 1024, 128, padding=64, groups=16))at sequence length 499, module alone, synthetic weights, macOS arm64, torch 2.13.0:sum,pow,convolution.ptesize is the same (33.6 MB) in both cases: the folded weight replacesweight_v, it does not join it.Quantized paths checked with and without the hook, Q/DQ op set and
.ptesize identical on all seven: legacy 8da4w,quantize_8da4w / 8da8w / int4 weight-only, PT2E static / dynamic / qc4w.Voxtral realtime, CI export path (streaming, 8da4w linears, 8w embedding, XNNPACK) on a 48x scaled-down random-weight instance of the same graph: the previous revision fails with the CI assertion, this revision lowers, with the same delegate count per method as main (21 / 13 / 0).
Test plan
backends/xnnpack/test/test_xnnpack_partitioner.py:test_parametrized_weight_is_folded_before_partitioning: aweight_normConv1d lowers to a single delegate call with nothing else at the top level, and the runtime output matches eager.test_pre_decomposition_folding_keeps_quantization_primitives: on aconvert_pt2egraph and on a groupwise int4 weight, the set ofquantized_decomposednodes is identical before and after the hook.test_pre_decomposition_folding_skips_factory_ops: every op in the skip set is checked to decompose tofull/full_likeand to survive the hook.test_pre_decomposition_folding_keeps_mutated_buffer: a buffer read and written in place stays a mutated buffer through the hook and through lowering, and the runtime carries its state across calls like eager.Also run locally with the hook in place:
backends/xnnpack/test/ops/test_conv1d.py,backends/xnnpack/test/ops/test_linear.py,exir/program/test/test_program.py,exir/tests/test_passes.py -k constant_prop.