Repository navigation
Fold parameter-only subgraphs before XNNPACK partitioning - #22391
john-rocky wants to merge 17 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 |
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. |
|
Thanks @john-rocky, running CI again |
|
Thanks for re-running CI, @nil-is-all. Two of the three jobs that failed on that run were my new tests, and the fix is pushed (428ffae); the third, Both tests passed a temporary The new commit is waiting on the workflow approval again. Thanks! |
|
findings from an agent-assisted review:
|
428ffae to
4e7e479
Compare
|
Thanks for the agent-assisted pass, @JakeStevens. All three reproduce on the previous head, and the fix is pushed (4e7e479, rebased on main): buffers and training graphs stay out of the fold, and the pass no longer lifts a scalar result. 1. Buffers shared across methods. The hook sees one method, so it cannot know which buffers another method writes. It now folds parameters and lifted tensor constants only: 2. Training exports. The hook returns a program with a 3. Scalar The seven red jobs on the last run were not this branch. Both lora jobs passed every test ( Thanks again for the careful look. If you would rather have |
|
thanks for the back and forth. few more things:
|
4e7e479 to
529dcf8
Compare
|
Thanks, @JakeStevens. Both reproduce on the previous head, and the fix is pushed (529dcf8, rebased on main). 1. 2. Names and meta. The naming applies to every caller, so the edge-level tests that asserted Rerun with the new pass: the partitioner file 16/16, Thanks again for the careful look. |
|
@john-rocky could you resolve the failing CI test? |
|
as we go back and forth here, I am coming to the idea that we should restrict the blast radius a bit.. Right now, the hook folds every parameter-only subgraph in the graph. But the weight_norm goal is narrower: make a computed weight static so a partitioner config will accept it. A parameter-derived model output, a branch through a custom op with no host kernel, an arange mask, an expand -- none of them feed a static weight, none unlock a delegation, and all of them are now folded and executed on the export machine on every XNNPACK export and hitting some issues in my testing like size regressions: 512× on an expand and 234× on an arange. One potential approach: GEMMConfig carries weight_idx and bias_idx, and _get_weight_deps already tests is_param_node on the weight. So the hook can seed from the nodes the enabled configs handle, take the weight and bias positions where is_param_node is false, walk back through constant-only producers, and hand that node set to constant_prop_pass as an allowlist alongside custom_skip_targets. |
529dcf8 to
7a70878
Compare
|
Thanks, @JakeStevens. All four reproduce on the previous head, and the fix is pushed (7a70878, rebased on main). The scope is narrowed as you sketched, and the three findings were mine: the insert point, the duplicated source and the shared name all came out of the round-3 registration change. 1. Scope. The hook seeds from the weight and bias positions of the ops the GEMM configs partition ( 2. Insert point. Fixed at the root, both ways you named: a folded placeholder goes after the last placeholder whose value lives in the same store (state dict or constants), the values are registered once every placeholder exists, in graph order, and an assertion at the end of the pass checks that the two orders agree. I cannot run the Qualcomm job here (no QNN bindings), so 3. Duplicated source. Folds that share a source are decided as a group, and the group is applied only if the folded tensors take no more bytes than the sources the fold erases. Your two shapes, as 4. Shared name. The name now derives from the source and from the expression: One step further on blast radius. The round-3 naming and kind change applied to every caller, and it should not have: Samsung's Rerun: Thanks again for the careful look. |
JakeStevens
left a comment
There was a problem hiding this comment.
Some notes inline.
These are about lines the change does not touch, so they could not be attached to one.
In exir/passes/constant_prop_pass.py, around line 634:
This helper always builds the edge clone. That was fine while every caller handed the pass an edge program. The new pre-decomposition hook hands it an ATen program, so a method that returns a parameter or a constant buffer directly ends up with an edge operator among plain ATen ops. It survives the default path only because the caller retraces straight afterwards, which is luck rather than design, and a partitioner listed after this one would see it. Please choose the clone from the dialect the graph is actually in, or skip the fixup before decomposition. While you are in there, this helper builds its constant set with the default buffer setting, so a read-only buffer output still gets a clone even when the caller passed the buffer switch off.
91e7d69 to
b406d10
Compare
|
Thanks, @JakeStevens. Three of the four inline findings reproduce on the previous head, and the naming one is as you describe. The fixes are pushed, one commit per finding (fca6649, ea434b3, fae30cd, f49140e, b406d10). The branch is rebased on main, which also brings in main's torchao pin. Both points of the note in your review body are as you describe, and b406d10 fixes them. The helper now picks The details of the four inline findings are on the threads. The shared-parameter fix has one cost: two methods that would fold the same expression over one shared parameter, like the prefill and decode case from an earlier round, now both keep the op. If you would rather fold there, I can make the decision across all methods instead. Rerun on the rebased head: PR comment (JA) @JakeStevens さん、ありがとうございます。インラインの4件のうち3件は前の head で再現し、命名の件はおっしゃるとおりでした。修正は指摘ごとに1コミットで push しました(fca6649、ea434b320、fae30cd26、f49140e43、b406d1015)。ブランチは main に rebase 済みで、main の torchao の pin もこれで取り込んでいます。 レビュー本文のメモは2点ともおっしゃるとおりで、b406d1015 で直しました。ヘルパーは、プログラムの dialect が ATen か training なら インラインの4件の詳細は各スレッドに書きました。共有 parameter の修正にはコストが1つあります。1つの共有 parameter に同じ式を fold するはずの2つのメソッドは、今は両方とも op を残します。以前のラウンドで出た prefill と decode のケースがこれに当たります。そこも fold したい場合は、判断をすべてのメソッドをまたいで行う形に変えられます。 rebase 後の head での再実行: |
b406d10 to
7dd7502
Compare
|
@john-rocky could you rebase and ping for review? |
7dd7502 to
9acd5c3
Compare
|
@nil-is-all Rebased onto main (02b9df3, 2026-10-15). The 17 commits replayed without conflicts and the patch is unchanged. The constant_prop_pass, XNNPACK partitioner and program tests pass locally on the rebased head. @JakeStevens could you take another look when you have a moment? |
|
@nil-is-all CI on the rebased head (9acd5c3) has finished: 170 checks passed, 30 skipped, none failed. The PR is ready for review. |
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.
_load_for_executorch_from_buffer keeps a pointer into the Python buffer and loads the method on the first call, so passing a temporary to_executorch().buffer left the module reading freed memory. On the second pull run that showed up as "Failed to execute method forward, error: 0x23" (unittest / macos) and a worker segfault (unittest-editable / linux), both gone on the re-run. With MallocScribble=1 both tests fail every time with "Program identifier 'UUUU' != expected 'ET12'"; holding the program manager for the life of the module fixes that. The mutated-buffer test no longer compares the first call, since the initial state of a mutated buffer is not serialized, and both runtime checks use an explicit atol.
The hook sees one method at a time, so a buffer it only reads can be written by another method of the same program. constant_prop_pass gains fold_buffers, and the hook passes False: parameters and lifted tensor constants seed the fold, buffers never do, and a reader keeps the shared allocation of a buffer another method writes. A joint forward-backward graph is returned untouched, since its parameters have to stay inputs for the runtime to hand them to the optimizer.
The lifted constant was named _prop_tensor_constant{N} from a per-method
counter and took its meta from the arithmetic node, so a weight tagged
for an external file lost the tag in the fold, and two methods whose
first fold came from different parameters wrote the same name into the
external constant map, where the second overwrote the first. The folded
value is now registered like the first placeholder it is computed from:
named {source}_prop, with the source's custom meta, and of the source's
kind. The kind matters at this hook because run_decompositions carries
the collected meta back only for parameters and buffers; a lifted tensor
constant comes out of it without its custom meta.
…it was The hook seeds from the weight and bias positions of the ops the GEMM configs partition, walks back through the constant-only producers, and hands that set to constant_prop_pass as an allowlist. A group of folds is applied only if the folded tensors take no more bytes than the sources the fold erases, so a parameter that is also used outside the fold is not duplicated. A program with no computed GEMM weight leaves the hook untouched. The pass registers a folded value like its source only when asked (register_like_source), so the edge-level callers keep the _prop_tensor_constant names and the constant kind. In that mode the name derives from the source and the expression, so two methods folding one parameter through different expressions no longer share a name in the external constant map. Folded placeholders are inserted after the last placeholder of the same store and registered in graph order, with an assertion that the two orders agree.
The external constant map is shared by the methods of a program and the data is deduplicated by content, so a name that already points at other data is a second method writing a different tensor under the same name; the last write was loaded for both.
The pass keeps a result that is not a tensor, the Python float of aten.item, in the propagated dictionary so that its consumers fold, and leaves the op in the graph only when a node outside the fold reads it. These tests pin that at the edge level, where `b * w.max().item()` comes out as one constant, and for a weight scaled by a scalar in the XNNPACK hook.
The expression string that names a folded value walked the producers recursively and described every occurrence again, and each level went through repr of the level below, which doubled the escaping as well. A chain of repeated squarings reached 2.5 GB of string at sixteen levels, a plain chain of fifty additions did not finish, and a deep chain hit the recursion limit. The walk is now iterative and emits a flat list of numbered definitions, one per producer, so the string grows with the number of producers and the name of a given expression is still the same in every method and export.
…tensor leaves The size check of the XNNPACK hook read the val meta of a node and called numel on it, but a node in the fold is not always one tensor. A split, chunk, unbind or max with a dim ended up in the materialized list as soon as one output fed the fold and another did not, with a list or a tuple for a val, and a scalar shared by the weight expression and a consumer outside it carried a SymFloat; graphs that lower on main failed with an attribute error. The walk now brings every getitem of a multi-output op into the fold, so the outputs are materialized one tensor each and the op never is; a scalar with a consumer outside the fold is accounted as the op it stays, with its tensor inputs materialized; the byte count sums the tensor leaves; and a group whose materialized value is still not one tensor is left alone.
The early exit asked whether some GEMM weight argument was not a parameter, buffer or lifted constant, which is true of every quantized model, since the weight arrives through a dequantize, and of a linear whose weight is an input or a matmul of two activations. All of them were functionalized in the hook, folded nothing, and were functionalized again by the caller. The hook now runs the constant-only walk on the program it is given and retraces only when a fold survives it. The one shape this misses is a weight built with an in-place op, which is impure before functionalization and pure after; it stays an op, and the comment says so.
The root pytest configuration ignores exir/tests/test_passes.py, and so does the Windows one, since pytorch#14037 (a test in it needs the custom ops library), and no workflow names the file, so the tests added to it for the pass options never ran in CI. They now live in exir/tests/test_constant_prop_pass.py, which the exir test path picks up on Linux, macOS and Windows, with a Buck target of their own: the allowlist, the buffer switch, the source-based registration, the scalar in a constant chain, the generated name and the graph-order check.
The walk in the XNNPACK hook recursed once per producer, so a weight computed through a chain of a few hundred ops reached the recursion limit and failed the export. It is now a post-order over an explicit stack, like the expression walk, with the same memo shared across seeds.
…ression The expression named each target without its namespace and the name kept eight hex characters of the digest, so two different folded tensors could share one name and break multi-method emission. Targets are now described fully qualified, aten.t.default or _operator.getitem, and the name keeps all 64 hex characters of the sha256 digest.
A fold took its external file tag, its kind and its name from the first source placeholder in graph order, so a merged adapter linear whose base is tagged for one file and whose adapter factors for another came out as one parameter in the base file with the adapter file empty. The pass now folds a node only when its sources agree on their custom meta, in both registration modes, and the XNNPACK hook applies the same rule when it picks the weights to fold. A fold registered like its sources is a parameter if any source is one, else a buffer if any is one, named after the first source of that kind.
The XNNPACK hook sees one method at a time, so a parameter one method folds and another still reads was stored twice: a tied embedding looked up in one method and multiplied transposed in another doubled the program. Before the pre-decomposition transforms run, EXIR now marks each constant placeholder that more than one method reads, matched by fully qualified name, with meta["shared_across_methods"]. The hook leaves the marked parameters and lifted constants out of the fold, as it does buffers.
The fixup that returns a constant output through a clone builds its constant set with the caller's fold_buffers, so a read-only buffer output is left alone when buffers are not folded. The tests also pin the clone for a program that is not in the edge dialect yet.
9acd5c3 to
e212176
Compare
|
@nil-is-all Rebased onto main (75af78c). Where main now does the same thing, the PR uses main's code. @JakeStevens #24352 describes itself as a smaller alternative to this PR. Should this stay open too? If it stays, let me know when someone can approve the CI runs and I'll push again. New runs here disappear about 48 hours after their last update. |
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 fold those weights before decomposition, so the fold is XNNPACK-scoped. The one change into_edge_transform_and_loweris a mark on the constants that more than one method reads. Its signature is unchanged. The hook seeds from the weight and bias positions of the ops the GEMM configs partition (linear, the conv family,addmm,mm,matmulwith a 2-d weight), walks back through the constant-only producers, and hands that node set toconstant_prop_passas an allowlist. The walk keeps an explicit stack, so a weight computed through hundreds of producers folds instead of reaching the recursion limit. A parameter-only subgraph anywhere else unlocks no delegation and stays an op; a program in which no weight will fold leaves the hook untouched, without the retrace.Seven 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. Folding a view of a buffer that is written to leaves the write on a constant, andrun_decompositionsfails on the aliasing (expected compiled_fn to be GraphModule), which is what the Voxtral realtime job hit on an earlier revision. The hook functionalizes first withrun_decompositions({}), the callto_edge_transform_and_lowermakes right after it anyway, and only when the constant-only walk on the program as given finds a weight that will fold; a quantized model, whose weights arrive through a dequantize, leaves the hook as it came. The one shape this misses is a weight built with an in-place op, impure before functionalization: it stays an op. 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.constant_prop_passgainsfold_buffers(defaultTrue, existing callers unchanged) and the hook passesFalse, so parameters and lifted tensor constants seed the fold and buffers never do; a reader keeps the shared allocation of a buffer another method writes.to_executorchmakes fordo_quant_fusion_and_const_prop..pteunchanged at 133,904 bytes; an earlier revision doubled it).to_edge_transform_and_lowermarks each constant placeholder that more than one method reads withmeta["shared_across_methods"] = True, as documented inPartitioner.transform_for_pre_decomposition. Placeholders are matched by fully qualified name. One with no users is not a read, since a non-strict export lifts every parameter of the module into every method. The hook leaves marked parameters and lifted constants out of the fold, as it does buffers: the other method keeps the original, so a fold would store a second copy. AnEmbedding(512, 128)read by a lookup in one method and a transposed matmul in another keeps the.pteat 264,720 bytes (an earlier revision stored 526,976). The cost: two methods that fold the same expression over one shared parameter now both keep the op.splitor a gate/upchunkin a weight expression brings everygetitemwith it, so the outputs are materialized one tensor each and the op never is; the byte rule counts tensor leaves, and a scalar that a consumer outside the fold also needs is accounted as the op it stays.Changes in the pass itself:
nodes_to_fold, an optional allowlist;fold_buffers, as above.register_like_source(defaultFalse, soquant_fusion_pass, Qualcomm'sconvert_mha_to_shaand Samsung'sConstantPropPasssee the same names, kind, meta and placeholder order as before). With it, a folded value is a parameter if any of its sources is a parameter, else a buffer if any is one, else a lifted constant. It keeps its sources'custommeta, becauserun_decompositionscarries meta back for parameters and buffers only and a fold lifted as a constant lost its external-file tag. Its name derives from the first source of the fold's kind and from the whole sha256 digest of the expression,w_prop_followed by 64 hex characters for a value computed from the parameterw, so two methods folding the same expression share one entry in the external constant map and two expressions over one parameter get two; a name from the tensor contents would change with the weights and break loading a.ptdexported from other weights. The expression names each op fully qualified (aten.t.default,_operator.getitem) and describes each producer once, so the string is linear in the expression.custommeta, where the external-file tag lives. This holds in both registration modes. A merged adapter linear whose base weight is tagged for one file and whose adapter factors for another is therefore not folded whole: the sum stays an op, the base keeps its file, and the adapter product, whose sources agree, folds into a value tagged like them. The hook applies the same rule when it picks the weights to fold.fold_buffers, so a read-only buffer output is left alone when buffers are not folded.And one check in the emitter:
_save_to_external_constant_mapraises when a name already in the map points at different data. The map is shared by the methods of a program and deduplicated by content, so that case is two methods writing different tensors under one name, and the last write was loaded for both.Earlier revisions: a
constant_propflag onto_edge_transform_and_lower(reworked after review), then the hook without the functionalize step (broke the Voxtral export), then a fold of every parameter-only subgraph (narrowed after review). 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 earlier 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_folds_gemm_weights_only: next to aweight_normconv, anarangemask, anexpandand a parameter-derived output stay ops; only the conv weight is folded, the runtime matches eager, and a program with no computed GEMM weight comes back as the same object.test_pre_decomposition_folding_does_not_duplicate_shared_weights: a tied embedding and a two-use weight produce the same.ptebytes with the hook as without it.test_pre_decomposition_folding_keeps_parameters_shared_across_methods: anEmbedding(512, 128)read by a lookup in one method and a transposed matmul in another gives a.pteof the same size as without the hook, the matmul method keepsembedding.weight, and both methods match eager through the runtime. Aweight_normconv weight that only one method reads still folds while the other method reads the conv's bias.test_pre_decomposition_folding_handles_multi_output_ops: a packed weight throughsplit,chunk,unbindandmax(dim)with one output used outside a GEMM folds with every output, the op is gone, and the runtime matches eager.test_pre_decomposition_folding_handles_a_deep_weight_chain: a linear weight built from its parameter by 600 additions folds, the program lowers to a single delegate call, and the runtime matches eager.test_pre_decomposition_folding_retraces_only_with_a_fold: a quantized linear, a linear whose weight is an input and a matmul of two activations come back as the same object withrun_decompositionsnever called; aweight_normconv folds.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 stay an op at a linear's weight position.test_pre_decomposition_folding_keeps_mutated_buffer: a buffer read and written in place stays a mutated buffer through the hook and through lowering while the weight is folded, and the runtime carries its state across calls like eager.test_pre_decomposition_folding_keeps_buffers_shared_across_methods: a buffer one method writes and another reads as a matmul weight comes back from the hook as the same object, stays in the reader's signature, and withshare_mutable_buffers=Trueboth methods carry itsmem_id=2allocation.test_pre_decomposition_folding_skips_training_graphs: an_export_forward_backwardprogram keeps its parameter and emits__et_training_parameters_index_forward.test_pre_decomposition_folding_folds_through_a_scalar_item: a linear weight scaled byscale.item()folds, theitemgoes with it, and the runtime matches eager.test_pre_decomposition_folding_keeps_a_scalar_item_used_outside_the_fold: a float used in the weight expression and outside it: the weight folds, theitemand its parameter stay, and the runtime matches eager.test_pre_decomposition_folding_handles_view_of_parameter:x @ self.w.t()lowers to a single delegate and matches eager through the runtime.test_pre_decomposition_folding_keeps_external_weight_tags: a foundation and a lora weight tagged withdelegate_external_constants_pass_unliftedand folded throught()land infoundation.ptdandlora.ptd, with nothing in the.pte.test_pre_decomposition_folding_keeps_adapter_and_base_weights_apart: a linear whose weight is a base tagged forfoundation.ptdplus an adapter product tagged forlora.ptdcomes back from the hook as the same object.foundation.ptdholds the base weight,lora.ptdboth adapter factors, and the.pteand both files are the same as without the hook.test_pre_decomposition_folding_names_folds_after_their_expression: three methods folding one parameter, two throught()and one through a scaledt(), write two entries into the external constant map, and each method returns its own value through the.ptd.exir/tests/test_constant_prop_pass.py, new:exir/tests/test_passes.pyis ignored bypytest.iniandpytest-windows.inisince #14037, so the tests for the pass options live in a file theexir/test path collects, with a Buck target.test_constant_prop_pass_folds_through_a_scalar(b * w.max().item()at the edge level comes out as one add against one constant),test_constant_prop_pass_keeps_a_scalar_op_used_outside_the_fold,test_constant_prop_pass_fold_buffers_false,test_constant_prop_pass_nodes_to_fold,test_constant_prop_pass_registers_fold_like_its_source,test_constant_prop_pass_registers_buffer_fold_as_buffer,test_constant_prop_pass_names_folds_after_their_expression,test_constant_prop_pass_describes_each_producer_once(sixteen levels of squaring and a chain of 600 additions; each op is written with its namespace, and the fold's name carries the whole digest),test_constant_prop_pass_registers_folds_in_graph_order(three folds off one parameter, the graph module bound by position, in both registration modes),test_constant_prop_pass_keeps_sources_tagged_for_different_files(a merged adapter weight tagged for two files: the sum stays an op and only the adapter product folds, taggedlora.ptd; with one tag on all three the whole weight folds),test_constant_prop_pass_clones_a_constant_output_in_the_dialect_of_the_graph(aten.clonebeforeto_edge, the edge clone after),test_constant_prop_pass_fold_buffers_false_leaves_a_buffer_output_alone. The existing tests intest_passes.pykeep their_prop_tensor_constant0asserts.exir/emit/test/test_emit.py:test_constant_tagged_tensor_same_name_different_data.exir/tests/test_partitioner_pre_decomposition.py:test_constants_read_by_several_methods_are_marked_for_the_transforms(the parameter both methods read is marked in each, the ones only one method reads are not, and a single-method program marks nothing).Also run locally with the hook in place:
backends/xnnpack/test/ops/test_conv1d.py,backends/xnnpack/test/ops/test_linear.py,backends/xnnpack/test/quantizer/test_pt2e_quantization.py,exir/program/test/test_program.py,exir/emit/test/test_emit.py,exir/tests/test_partitioner_pre_decomposition.py,exir/tests/test_passes.py -k constant_prop.cc @GregoryComer @digantdesai @cbilgin @JakeStevens @nil-is-all