Skip to content

Only rewrite addmm/mm to linear when the weight is a constant - #22402

Open
msluszniak wants to merge 12 commits into
pytorch:mainfrom
msluszniak:ms/addmm-to-linear-constant-weight
Open

msluszniak wants to merge 12 commits into
pytorch:mainfrom
msluszniak:ms/addmm-to-linear-constant-weight

Conversation

@msluszniak

@msluszniak msluszniak commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

replace_addmm_mm_with_linear rewrites mm(x, transpose(w)) into linear(x, w) whenever the second operand is fed through a transpose. It never checks where w comes from, so a matmul against a tensor computed at runtime becomes a linear too.

That rewrite is not sound. mm/addmm place no constraint on their second operand, but backends prepack a linear's weight while building their delegate graph, which is only possible for a constant. The Vulkan runtime aborts when it gets anything else:

Exception raised from toTensorRef at
backends/vulkan/runtime/graph/containers/Value.h:266:
(isTensorRef()) is false! Expected value to have type TensorRef, got TENSOR instead.

reached through linear_packed_weight -> prepack_fp_linear_weight -> PrepackNode, at delegate build time. The model fails to load, and the message names neither the op nor the operand, so there is nothing to work back from.

This shows up in any model that multiplies an activation by a runtime-derived matrix. It was found in a TTS duration predictor whose mm second operand is permute_copy(squeeze_copy(...)), a 64x64 style-conditioned matrix produced during execution. Two other addmm nodes in the same graph have real parameter weights and rewrite correctly; only the computed one is affected.

Fix

Guard both rewrites on the operand actually being a parameter, buffer or lifted constant, and thread the owning program through so that check can be made. Callers that pass no program get no rewrite for placeholder weights, parameters included; every in-tree caller passes it, and the pass warns once when it skips one.

The pass is used by the Vulkan and Samsung ENN backends.

Test plan

New backends/transforms/test/test_addmm_mm_to_linear.py, four cases: a constant weight still rewrites to linear, a computed mm operand stays mm, a computed addmm operand stays addmm, and a user-input weight is left alone.

Verified against the reported model on a Galaxy S26 Ultra (Adreno 840). Before, the delegate aborts at build time. After, it loads and runs, and all four sub-models of that TTS pipeline match their CPU references:

sub-model cosine vs CPU
duration_predictor 1.000000 (was: abort)
text_encoder 0.999414
vector_estimator 0.999994
vocoder 0.999977

Confirmed the guard is what changes the outcome: on the unpatched pass the same graph yields linear=1, mm=0; patched it yields linear=0, mm=1.

cc @cccclai

@pytorch-bot

pytorch-bot Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⚠️ 22 Awaiting Approval

As of commit 096f701 with merge base b50c571 (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 Sep 1, 2026
msluszniak added a commit to software-mansion-labs/executorch that referenced this pull request Sep 1, 2026
Backport of pytorch/executorch#22402.

replace_addmm_mm_with_linear rewrote mm(x, transpose(w)) into linear(x, w)
without checking that w is a constant, so a matmul against a runtime-computed
matrix became a linear whose weight the Vulkan runtime then tried to prepack
at delegate build time:

  (isTensorRef()) is false! Expected value to have type TensorRef,
  got TENSOR instead.

Guard both rewrites on the operand being a parameter, buffer or lifted
constant.
@github-actions

github-actions Bot commented Sep 1, 2026

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.

@SS-JIA SS-JIA left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review. Inline findings below.

return True
return (
is_param(exported_program, node)
or is_buffer(exported_program, node)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] is_buffer() also accepts buffers listed in graph_signature.buffers_to_mutate. Those values are state, not build-time constants; rewriting a matmul that reads one into Vulkan linear can prepack an old value and return wrong results after the buffer changes. Exclude mutable buffers here and add a multi-invocation regression.

AI reviewer note: this comment was generated by an AI reviewer.

# Without the owning program a lifted parameter cannot be told apart
# from a user input. Placeholders were always rewritten before, so keep
# accepting them rather than regressing callers that pass no program.
return True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Returning true here leaves user-input weights unprotected for callers that omit the program. Samsung currently constructs AddmmToLinearTransform() without one, and XNNPACK calls the helper without one, so this pass still rewrites runtime placeholder weights there. Thread the owning program through those call sites, or conservatively reject placeholders when it is unavailable.

AI reviewer note: this comment was generated by an AI reviewer.

@msluszniak

Copy link
Copy Markdown
Contributor Author

Mutated buffers are now rejected, and the owning program is threaded through Vulkan, Samsung and XNNPACK so the no-program case can refuse placeholders instead of accepting them; @nil-is-all could you help find a reviewer?

@msluszniak

Copy link
Copy Markdown
Contributor Author

@pytorchbot label "module: backend"

@pytorch-bot pytorch-bot Bot added the module: backend Issues related to backend APIs or requests for new backends label Sep 14, 2026
`replace_addmm_mm_with_linear` turns `mm(x, transpose(w))` into
`linear(x, w)` whenever the second operand is fed through a transpose. It
never checks where `w` comes from, so a matmul against a tensor computed at
runtime is rewritten into a linear too.

That rewrite is not sound. `mm`/`addmm` place no constraint on their second
operand, but backends prepack a linear's weight while building their delegate
graph, which is only possible for a constant. The Vulkan runtime aborts when
it gets anything else:

  Exception raised from toTensorRef at
  backends/vulkan/runtime/graph/containers/Value.h:266:
  (isTensorRef()) is false! Expected value to have type TensorRef,
  got TENSOR instead.

reached through linear_packed_weight -> prepack_fp_linear_weight ->
PrepackNode, at delegate build time. A model that multiplies an activation by
a runtime-derived matrix fails to load with no indication of which op is
responsible.

Guard both rewrites on the operand actually being a parameter, buffer or
lifted constant, and thread the owning program through so that check can be
made. Callers that pass no program keep the previous behaviour for
placeholders and only lose the rewrite for runtime-computed operands, which is
the case that was broken anyway.

The pass is used by the Vulkan and Samsung ENN backends.
is_buffer() also accepts a buffer the program writes back to. That is state,
not a build-time constant: a delegate that prepacked it would keep returning
the value it was built with. Reject buffers named in buffers_to_mutate.

Accepting placeholders when no program was passed left user-input weights
unprotected for the callers that omit it. Vulkan, Samsung and XNNPACK all have
the owning program in scope, so pass it (ConvertToLinearPass becomes an
XNNPACKPass, which the pass manager already hands the program), and make the
no-program case reject placeholders instead.
@msluszniak
msluszniak force-pushed the ms/addmm-to-linear-constant-weight branch from a8d79c8 to 3ebf9e2 Compare September 15, 2026 11:07
A quantized weight reaches mm/addmm through a dequantize node, which is a
call_function, so is_constant_tensor read it as a runtime value and the
rewrite to linear was skipped.

That silently disables downstream quantized-matmul fusion, which matches on
linear. Exporting all-mpnet-base-v2 int8 for Vulkan produced 72
dequantize_per_channel plus 72 float addmm instead of 72
_weight_int8pack_mm, dequantizing the weights at runtime and aborting at
load because the Vulkan runtime has no dequantize_per_channel kernel.

Accept a dequantize node when everything it dequantizes is itself constant.
Both mpnet int8 Vulkan exports are then byte-identical to the published
artifacts again.
@digantdesai

Copy link
Copy Markdown
Contributor

Still a WIP?

ConvertToLinearPass became an XNNPACKPass in this stack, so its constructor
now takes the exported program. This call site was still constructing it with
no arguments and raised

  TypeError: XNNPACKPass.__init__() missing 1 required positional argument:
  'exported_program'

Every llama-family export goes through here, which is why it took out the
llama runner, lora, qnn and phi_4_mini jobs along with the export_llama_lib
unit tests.
@msluszniak

Copy link
Copy Markdown
Contributor Author

@digantdesai No, it is ready for review :))

@digantdesai

Copy link
Copy Markdown
Contributor

LGTM, CI is still red, not sure if its your PR or main is red? Please take a look?

@msluszniak

Copy link
Copy Markdown
Contributor Author

I think the reason why gitlab fails is different than my code changes ;p
image

@msluszniak

Copy link
Copy Markdown
Contributor Author

However, this one check is on my side: https://github.com/pytorch/executorch/actions/runs/35065274307/job/104849342287?pr=22402, I'm fixing it now.

EdgeProgramManager.transform() runs its passes over every method, so passing
a single program bound one pass to every graph, and exported_program() with no
argument looks up "forward". A multimethod export has no such method, so
test-lora-multimethod failed with KeyError: 'forward' before any rewriting
happened.

Use the per-method dict form of transform() so each pass gets the program of
the graph it rewrites.
@msluszniak

Copy link
Copy Markdown
Contributor Author

@digantdesai it should be fine now.

@executorch-triage executorch-triage Bot added the community: contribution PRs coming from community (excluding hardware partners) label Sep 22, 2026
@digantdesai

Copy link
Copy Markdown
Contributor

@claude review this PR

@digantdesai

Copy link
Copy Markdown
Contributor

rebased to retrigger the CI

@claude

claude Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Claude finished @digantdesai's task in 3m 47s —— View job


The constant-weight guard is sound, but the local dequant allowlist leaves out dequantize_per_channel_group, so 4-bit groupwise mm/addmm weights would stop being rewritten to linear; the PR description also needs updating for the no-program case.

Detailed review

PR Review: #22402

Summary

The PR rewrites mm/addmm to linear only when the transposed operand is a parameter, a non-mutated buffer, a lifted constant, or a dequant of one. It also passes the owning ExportedProgram through the Vulkan, Samsung and XNNPACK call sites and the LLM builder. Both points from the earlier AI review are fixed: mutated buffers are now rejected, and the no-program path now rejects placeholders. Two problems are left, listed below.

Backward Compatibility

  • backends/transforms/addmm_mm_to_linear.py:49-55: the dequant allowlist is missing dequantize_per_channel_group. XNNPACK (backends/xnnpack/utils/quant_utils.py:30) and Vulkan (backends/vulkan/utils.py:34) both list it as a weight dequant op. It is what legacy 4-bit groupwise quantization (8da4w-style) produces. A mm(x, t(dequantize_per_channel_group(w_int, ...))) used to be rewritten, but now is_constant_tensor returns False, because the target is a call_function that is not in _DEQUANT_TARGETS. The result is that XNNPACK's ConvertToLinearPass fallback no longer turns it into a quantized linear. Rather than keeping a third hand-written copy of this set, reuse an existing one, or at least add the missing op and a test for it. Fix this →
  • Low confidence: quantize_* ops are not followed either. If a weight was never constant-folded (for example convert_pt2e(..., fold_quantize=False)), the operand is dq(q(param)). The recursion stops at the q node and returns False, so the weight is treated as runtime data. I haven't confirmed that any in-tree flow reaches this pass with unfolded weight Q/DQ. If one does, treat a quantize_* node whose inputs are all constant as constant too.
  • The PR description no longer matches the code. It says "Callers that pass no program keep the previous behaviour for placeholders". That was true for the first version; the current code (addmm_mm_to_linear.py:79-84) rejects every placeholder when no program is passed. Out-of-tree callers of the public apply_addmm_mm_to_linear_transform(graph) exported from backends/transforms/__init__.py, or of AddmmToLinearTransform(), will silently get no rewrite for parameter weights. Please update the description so the release note is accurate. Logging a one-time warning when exported_program is None and a placeholder candidate is skipped would make that failure mode visible.

Testing

  • backends/xnnpack/test/passes/test_convert_to_linear.py:72-73: assertRaises(KeyError): edge.exported_program() checks how EdgeProgramManager resolves a default method name, not anything about this pass. It will break if that default changes, so drop it.
  • backends/transforms/test/test_addmm_mm_to_linear.py:76: the name test_computed_bias_operand_stays_an_addmm is misleading, because the computed operand is the weight ((w * 2.0).t()) and the bias is a plain user input. Rename it to something like test_computed_addmm_weight_stays_an_addmm.
  • No test covers the negative dequant case: a dequant whose input is computed at runtime should leave the matmul unchanged. That is the recursive branch at addmm_mm_to_linear.py:71-77.

Code Quality

  • addmm_mm_to_linear.py:37-41: _is_mutated_buffer builds a set from buffers_to_mutate.values() on every call only to test membership once. name in signature.buffers_to_mutate.values() is enough.

Recommendation

Request Changes. Add dequantize_per_channel_group, ideally by reusing an existing DQ set, and update the PR description. Everything else is minor.

dequantize_per_channel_group (4-bit groupwise) was missing from the
dequant allowlist, so those mm/addmm weights stopped being rewritten.
msluszniak and others added 5 commits October 12, 2026 15:10
An unfolded weight reaches mm as dq(q(param)); a quantize node whose
inputs are all constant is constant too.
Without the ExportedProgram the pass leaves parameter weights alone; log it
so out-of-tree callers notice the missing rewrite.
Rename the computed-weight addmm test, cover a dequant of runtime data, and
drop an assertion that only tested EdgeProgramManager's default method.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@digantdesai

Copy link
Copy Markdown
Contributor

@msluszniak want to fix the thing Claude suggested? I can merge it then :)

@msluszniak

Copy link
Copy Markdown
Contributor Author

@digantdesai All the suggestions from claude review had been addressed a that moment ;)

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. community: contribution PRs coming from community (excluding hardware partners) module: backend Issues related to backend APIs or requests for new backends

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants