Skip to content

Only fuse an RMS norm whose weight can be prepacked - #22777

Open
giuliocorradi wants to merge 1 commit into
pytorch:mainfrom
giuliocorradi:vulkan-rms-norm-const-weight
Open

Only fuse an RMS norm whose weight can be prepacked#22777
giuliocorradi wants to merge 1 commit into
pytorch:mainfrom
giuliocorradi:vulkan-rms-norm-const-weight

Conversation

@giuliocorradi

Copy link
Copy Markdown

Fixes #22773

Summary

et_vk.rms_norm prepacks its weight, so the multiply that the fusion folds in has to be a constant the prepacker can see. RmsNormMatch folded in whatever multiply followed the norm, without checking, so a multiplier computed in the graph produced an op the runtime aborts on at the first inference:

prepack_standard at backends/vulkan/runtime/graph/ops/impl/Staging.cpp:229:
  (graph.val_is_tref(tensor_data)) is false!

Two common shapes hit this:

  • Adaptive normalization — a norm whose scale is produced at inference from a conditioning signal. This is AdaLN / AdaLN-Zero, so it covers DiT and most diffusion transformers, and π₀.₅, where the scale comes from the flow-matching timestep.
  • Gemma's ordinary RMSNorm, written norm(x) * (1.0 + weight). The multiplier is constant valued, but it is an intermediate node, not a leaf, so it is equally unprepackable.

So the deciding property is not "constant vs. computed" — it is whether the multiplier is something the prepacker can resolve to a constant tensor.

The change

Reject a non-prepackable weight. This is done in two places because the two callers see different things:

  • The detector (RmsNormMatch.__init__) is handed only a node — get_all_fusable_subgraphs has no ExportedProgram — so it requires the weight to be a placeholder. That covers every computed multiplier, including both cases above.
  • The replacement is handed the exported program, so the remaining distinction — a constant placeholder vs. a user input, which are indistinguishable in the graph alone — is made there with utils.is_param_node.

Refusing the match is safe: the norm and the multiply are ordinary supported ops, so the pattern stays unfused and costs one extra dispatch. Note the weight is not part of match.all_nodes, so declining the match does not leave the partitioner holding nodes nothing will replace.

Test plan

python -m unittest backends.vulkan.test.test_vulkan_passes -v

12 tests pass. The new test, test_rms_norm_fuses_only_prepackable_weight, fails without the source change, in both of its negative subtests:

AssertionError: 1 != 0 : expected 0 fused rms_norm: 1.0 + w is an intermediate node
AssertionError: 1 != 0 : expected 0 fused rms_norm: a graph input is not a constant

Also verified end to end on an AMD Radeon 8060S (RADV GFX1151, RDNA 3.5), lowering each module and comparing the delegate's output against eager:

multiplier before after
leaf parameter fused, ok fused, ok (unchanged)
leaf buffer fused, ok fused, ok (unchanged)
parameter through a dtype cast prepack abort unfused, ok
1.0 + parameter (Gemma) prepack abort unfused, ok
graph input (adaptive norm) prepack abort unfused, ok
computed by a layer (AdaLN) prepack abort unfused, ok

The dtype-cast row is worth calling out: the pattern's own docstring describes the Llama-style graph where the weight reaches the multiply through a to_copy, and that form aborts today whenever the cast is not constant-folded before the pass. Requiring the weight node itself to be a placeholder covers it, at the cost of not fusing that form — which is what already happens, except now it runs instead of aborting. If fusing through a weight cast is wanted, that seems better as a separate change that prepacks the underlying constant deliberately.

backends.vulkan.test.test_vulkan_delegate produces an identical set of results before and after (its failures in my environment are pre-existing and unrelated).

Found while lowering openpi's π₀.₅ to the Vulkan delegate.

et_vk.rms_norm prepacks its weight, so the multiply that the fusion folds
in has to be a constant the prepacker can see. The pattern folded in any
multiply that followed the norm, so a multiplier computed in the graph
produced an op the runtime aborted on at the first inference:

  prepack_standard ... (graph.val_is_tref(tensor_data)) is false!

This affects any adaptive normalization - a norm whose scale is produced
at inference from a conditioning signal, as in DiT-style AdaLN - and also
Gemma's ordinary RMSNorm, whose scale is written `1.0 + weight` and is
therefore constant valued but still an intermediate node.

Reject a non-prepackable weight in two places, because the two callers
see different information. The detector is given only the graph, so it
can require the weight to be a placeholder, which covers every computed
multiplier. Only the replacement is given the exported program, so the
distinction between a constant placeholder and a user input is made
there.

Not fusing is correct: the norm and the multiply are both supported ops,
so the pattern simply stays unfused and costs one extra dispatch.

Test Plan:

  python -m unittest backends.vulkan.test.test_vulkan_passes -v

12 tests pass. The new test,
test_rms_norm_fuses_only_prepackable_weight, fails without this change
with "1 != 0 : expected 0 fused rms_norm" for both the computed-constant
and the graph-input case.

Verified end to end on an AMD Radeon 8060S (RADV GFX1151), lowering each
module and comparing the delegate's output against eager:

  multiplier                          before        after
  leaf parameter                      fused, ok     fused, ok
  leaf buffer                         fused, ok     fused, ok
  parameter through a dtype cast      prepack abort unfused, ok
  1.0 + parameter (Gemma)             prepack abort unfused, ok
  graph input (adaptive norm)         prepack abort unfused, ok
  computed by a layer (AdaLN)         prepack abort unfused, ok

backends.vulkan.test.test_vulkan_delegate shows an identical set of
results before and after the change.
Copilot AI lite review requested due to automatic review settings September 13, 2026 03:30
@pytorch-bot

pytorch-bot Bot commented Sep 13, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⚠️ 16 Awaiting Approval

As of commit ac0d5f3 with merge base 903696a (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 commented Sep 13, 2026

Copy link
Copy Markdown

Hi @giuliocorradi!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@linux-foundation-easycla

Copy link
Copy Markdown

CLA Not Signed

@github-actions

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.

Copilot AI 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.

🟡 Changes recommended

The replacement-level early-return can be counted as a successful replacement by the current pattern framework, potentially triggering unnecessary retrace/recompile work for models with many adaptive RMSNorms.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR fixes a Vulkan RMSNorm fusion correctness issue where the fusion could fold a non-prepackable multiplier into et_vk.rms_norm, causing the Vulkan runtime to abort during weight prepacking. The change tightens the fusion criteria so the folded-in multiplier is only accepted when it can be resolved as a prepackable constant/parameter in the exported program, and adds a regression test covering the previously-crashing shapes.

Changes:

  • Add a guard in the RMSNorm pattern detector to reject non-leaf (graph-computed) multipliers.
  • Add a guard in the RMSNorm replacement to reject placeholder multipliers that are actually user inputs (not params/buffers/lifted constants).
  • Add a unit test ensuring fusion only occurs for prepackable RMSNorm weights.
File summaries
File Description
backends/vulkan/patterns/rms_norm.py Tightens RMSNorm fusion eligibility to avoid folding non-prepackable multipliers; adds a final is_param_node check during replacement.
backends/vulkan/test/test_vulkan_passes.py Adds coverage for RMSNorm fusion only occurring when the folded multiplier is prepackable (and not an intermediate node or graph input).
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +281 to +286
# The detector only sees the graph, which cannot distinguish a constant
# placeholder from a user input; both look the same there. Only a constant
# is actually prepackable, so make the final check here, where the exported
# program is available.
if not utils.is_param_node(ep, match.weight_node):
return
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ET-VK] RMS-norm fusion folds a non-leaf multiplier and then prepacks it, breaking adaptive norms

3 participants