Skip to content

Fold DyT affine maps into following convolutions - #21953

Open
cgreenberg wants to merge 2 commits into
pytorch:mainfrom
cgreenberg:export-D116573000
Open

Fold DyT affine maps into following convolutions#21953
cgreenberg wants to merge 2 commits into
pytorch:mainfrom
cgreenberg:export-D116573000

Conversation

@cgreenberg

@cgreenberg cgreenberg commented Aug 19, 2026

Copy link
Copy Markdown

Summary:
Second half of the Dynamic Tanh (DyT) lowering cost, and a companion to
FoldDyTAlphaIntoLUTPass. Once the alpha multiply has been folded into the tanh
TABLE, a DyT site still emits a per-channel gamma Mul and a per-channel beta Add
between the TABLE and the convolution that consumes it. Both are full-tensor
elementwise ops on Ethos-U.

This adds FoldDyTAffineIntoConvPass in backends/arm/_passes/, which folds
that affine into the weights and bias of the following convolution, the same
algebra as BatchNorm folding:
conv(gamma * x + beta) == conv_with_scaled_weights(x) + conv(beta).

Doing this in floating point is not safe here, because the intermediate INT8
requantization between the affine and the convolution is nonlinear, so a float
fold can change rounding. The pass instead evaluates the site's real TOSA integer
path over the materialized 256-entry TABLE and only rewrites when the resulting
per-channel map is provably exactly integer-affine. Everything else fails closed:
saturating or nonlinear maps, unsupported constant layouts, non-exclusive
passthrough edges, rank or shape mismatches, and singleton-channel broadcast are
all rejected rather than approximated.

Padded convolutions are a special case. Gamma still folds exactly, but beta does
not: with constant padding its contribution becomes position dependent at the
boundary and cannot be represented by a single conv bias. For those sites the
pass folds gamma only when gamma is an exact identity and leaves the beta Add in
place.

Also exposes a small hook in insert_table_ops that the fold needs to locate the
materialized TABLE.

As with the alpha fold, register_pass_factories_before is imported inside
register_fold_dyt_affine_into_conv_pass() rather than at module scope, to avoid
a circular import through _passes/__init__.py.

The pass is inert until a model registers it, so this diff changes no behaviour
on its own.

Differential Revision: D116573000

cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani

@pytorch-bot

pytorch-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

❌ 3 New Failures, 1 Unrelated Failure

As of commit 05c0192 with merge base 7c3cdbb (image):

NEW FAILURES - The following jobs have failed:

  • Cadence Build & Test / hifi-build / hifi4 (gh)
    ##[error]Refusing to check out fork pull request code from a 'pull_request_target' workflow. This workflow runs with the base repository's GITHUB_TOKEN, secrets, default-branch cache scope, and runner access. Fetching and executing a fork's code in that trusted context commonly leads to "pwn request" vulnerabilities. To opt in, review the risks at https://gh.io/securely-using-pull_request_target and set 'allow-unsafe-pr-checkout: true' on the actions/checkout step.
  • Cadence Build & Test / vision-build / vision (gh)
    ##[error]Refusing to check out fork pull request code from a 'pull_request_target' workflow. This workflow runs with the base repository's GITHUB_TOKEN, secrets, default-branch cache scope, and runner access. Fetching and executing a fork's code in that trusted context commonly leads to "pwn request" vulnerabilities. To opt in, review the risks at https://gh.io/securely-using-pull_request_target and set 'allow-unsafe-pr-checkout: true' on the actions/checkout step.
  • pull / android / run-emulator (gh)
    java.lang.IllegalStateException: Cannot access system provider: 'settings' before system providers are installed!

BROKEN TRUNK - The following job failed but were present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@linux-foundation-easycla

linux-foundation-easycla Bot commented Aug 19, 2026

Copy link
Copy Markdown

CLA Not Signed

@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 Aug 19, 2026
@meta-codesync

meta-codesync Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

@cgreenberg has exported this pull request. If you are a Meta employee, you can view the originating Diff in D116573000.

@pytorch-bot

pytorch-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

Workflows were awaiting approval. CI has now been triggered for the ciflow labels on this PR.

Summary:
Dynamic Tanh (DyT) normalization computes `tanh(alpha * x)` with a learned scalar
alpha. Quantized, that lowers to a full-tensor integer Mul followed by a tanh
TABLE. On Ethos-U the TABLE is free but the Mul is a real per-element cost, so
the Mul is pure overhead.

This adds `FoldDyTAlphaIntoLUTPass` in `backends/arm/_passes/`, which folds the
alpha multiply into the tanh lookup table and deletes the Mul and its
surrounding rescales.

Folding alpha in floating point before quantization would change rounding, so the
pass does it in the integer domain instead: it replays the exact TOSA
SINGLE_ROUND RESCALE and Mul arithmetic over all 256 int8 input codes, feeds the
results through the existing tanh quantization mapping, and materializes the
result as one 256-entry TABLE. The rewrite is therefore byte-exact rather than
approximate. It fails closed on anything it cannot prove: non-scalar alpha,
activation-side rank views, and narrowed tanh ranges are all handled explicitly.

Also adds `register_pass_factories_before()` to the Arm pass manager, a small
hook for inserting a pass that needs access to the `ExportedProgram` ahead of a
named target pass. `FoldDyTAlphaIntoLUTPass` needs it to read constant tensors.

Note on one import: the pass imports `register_pass_factories_before` inside
`register_fold_dyt_alpha_into_lut_pass()` rather than at module scope.
`_passes/__init__.py` imports `arm_pass_manager` last and `arm_pass_manager`
imports back from the package, so a module-scope import from a pass module that
`__init__.py` re-exports is circular. Only the register helper needs it.

The pass is inert until a model registers it, so this diff changes no behaviour
on its own.

Differential Revision: D116560649
Summary:
Second half of the Dynamic Tanh (DyT) lowering cost, and a companion to
`FoldDyTAlphaIntoLUTPass`. Once the alpha multiply has been folded into the tanh
TABLE, a DyT site still emits a per-channel gamma Mul and a per-channel beta Add
between the TABLE and the convolution that consumes it. Both are full-tensor
elementwise ops on Ethos-U.

This adds `FoldDyTAffineIntoConvPass` in `backends/arm/_passes/`, which folds
that affine into the weights and bias of the following convolution, the same
algebra as BatchNorm folding:
`conv(gamma * x + beta) == conv_with_scaled_weights(x) + conv(beta)`.

Doing this in floating point is not safe here, because the intermediate INT8
requantization between the affine and the convolution is nonlinear, so a float
fold can change rounding. The pass instead evaluates the site's real TOSA integer
path over the materialized 256-entry TABLE and only rewrites when the resulting
per-channel map is provably exactly integer-affine. Everything else fails closed:
saturating or nonlinear maps, unsupported constant layouts, non-exclusive
passthrough edges, rank or shape mismatches, and singleton-channel broadcast are
all rejected rather than approximated.

Padded convolutions are a special case. Gamma still folds exactly, but beta does
not: with constant padding its contribution becomes position dependent at the
boundary and cannot be represented by a single conv bias. For those sites the
pass folds gamma only when gamma is an exact identity and leaves the beta Add in
place.

Also exposes a small hook in `insert_table_ops` that the fold needs to locate the
materialized TABLE.

As with the alpha fold, `register_pass_factories_before` is imported inside
`register_fold_dyt_affine_into_conv_pass()` rather than at module scope, to avoid
a circular import through `_passes/__init__.py`.

The pass is inert until a model registers it, so this diff changes no behaviour
on its own.

Differential Revision: D116573000
@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.

@zingo zingo added the partner: arm For backend delegation, kernels, demo, etc. from the 3rd-party partner, Arm label Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunk CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported module: arm Issues related to arm backend partner: arm For backend delegation, kernels, demo, etc. from the 3rd-party partner, Arm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants