Skip to content

Keep the shape operand of view_as and expand_as out of shared qparams (#24153) - #24153

Open
pssrawat wants to merge 2 commits into
pytorch:mainfrom
pssrawat:export-D127380012
Open

pssrawat wants to merge 2 commits into
pytorch:mainfrom
pssrawat:export-D127380012

Conversation

@pssrawat

@pssrawat pssrawat commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

SharedQspecQuantizer gives each shared-qparams cluster one set of qparams for every float input and output of its ops. view_as(x, other) is in that list, and the cluster was built from all of its inputs, so other was pulled in even though it contributes only its shape. x and other were then quantized with one scale, set by whichever has the wider range: the data loses precision, or the shape operand is clipped.

For example, in (x * y).view_as(other) + other with other = z + z, x * y spans about ±2.5 and other about ±51. x * y was quantized at other's scale of 0.349, about 18x coarser than the 0.0195 it gets when the same reshape is written reshape(4, 8), and the output error against float rose from 0.258 to 0.307.

expand_as has the same shape-only second operand but was not in the shared-qparams list at all, so every expand_as broadcast stayed in float behind a dequantize and a requantize.

Shape-as ops (view_as, expand_as) now share qparams only through their data operand, args[0]. The cluster walk does not cross the edge to the shape operand in either direction, and that operand gets no shared input qspec, so it keeps the qparams of its own producer. expand_as joins the shared-qparams ops, so like expand it is quantized to int8 when its neighbours are, and lowers on Cortex-M to an int8 expand_copy.

Authored with Claude Code.

Differential Revision: D127380012

@pssrawat
pssrawat requested a review from rascani as a code owner October 26, 2026 15:46
@pytorch-bot

pytorch-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

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

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

✅ No Failures

As of commit c8655ab with merge base 7586c16 (image):
💚 Looks good so far! There are no failures yet. 💚

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 Oct 26, 2026
@meta-codesync

meta-codesync Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

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

@meta-codesync meta-codesync Bot changed the title Quantize expand_as through the existing shared-qparams expand path Quantize expand_as through the existing shared-qparams expand path (#24153) Oct 28, 2026
pssrawat added a commit to pssrawat/executorch that referenced this pull request Oct 28, 2026
pytorch#24153)

Summary:

`SharedQspecQuantizer` already gives `expand` and `expand_copy` shared qparams
when a neighbor is quantized, and the lowering then runs `expand_copy` on int8.
`expand_as` is not in that list, and it only decomposes to `expand` at
`to_edge`, after quantization, so every `expand_as` broadcast stayed in float
behind a dequantize and a requantize.

`ExpandAsToExpandPass` runs in `transform_for_annotation` and rewrites
`expand_as(x, other)` into `expand(x, other.shape)`, emitting `sym_size` nodes
for symbolic dims. `expand_as` then gets the same neighbor-driven treatment as
`expand`: int8 when its neighbors are quantized, float otherwise. The shape-only
`other` operand is no longer an input of the op, so it is never pulled into the
shared-qparams cluster.

No new pattern check or support-dict entry is added for `expand`, so an expand
between float-only ops is not forced to int8.

Differential Revision: D127380012
@pssrawat pssrawat added the release notes: none Do not include this in the release notes label Oct 29, 2026
pssrawat added a commit to pssrawat/executorch that referenced this pull request Oct 29, 2026
pytorch#24153)

Summary:

`SharedQspecQuantizer` already gives `expand` and `expand_copy` shared qparams
when a neighbor is quantized, and the lowering then runs `expand_copy` on int8.
`expand_as` is not in that list, and it only decomposes to `expand` at
`to_edge`, after quantization, so every `expand_as` broadcast stayed in float
behind a dequantize and a requantize.

`ExpandAsToExpandPass` runs in `transform_for_annotation` and rewrites
`expand_as(x, other)` into `expand(x, other.shape)`, emitting `sym_size` nodes
for symbolic dims. `expand_as` then gets the same neighbor-driven treatment as
`expand`: int8 when its neighbors are quantized, float otherwise. The shape-only
`other` operand is no longer an input of the op, so it is never pulled into the
shared-qparams cluster.

No new pattern check or support-dict entry is added for `expand`, so an expand
between float-only ops is not forced to int8.

Differential Revision: D127380012
pssrawat added a commit to pssrawat/executorch that referenced this pull request Oct 29, 2026
pytorch#24153)

Summary:

`SharedQspecQuantizer` already gives `expand` and `expand_copy` shared qparams
when a neighbor is quantized, and the lowering then runs `expand_copy` on int8.
`expand_as` is not in that list, and it only decomposes to `expand` at
`to_edge`, after quantization, so every `expand_as` broadcast stayed in float
behind a dequantize and a requantize.

`ExpandAsToExpandPass` runs in `transform_for_annotation` and rewrites
`expand_as(x, other)` into `expand(x, other.shape)`, emitting `sym_size` nodes
for symbolic dims. `expand_as` then gets the same neighbor-driven treatment as
`expand`: int8 when its neighbors are quantized, float otherwise. The shape-only
`other` operand is no longer an input of the op, so it is never pulled into the
shared-qparams cluster.

No new pattern check or support-dict entry is added for `expand`, so an expand
between float-only ops is not forced to int8.

Differential Revision: D127380012
pssrawat added a commit to pssrawat/executorch that referenced this pull request Oct 29, 2026
pytorch#24153)

Summary:

`SharedQspecQuantizer` already gives `expand` and `expand_copy` shared qparams
when a neighbor is quantized, and the lowering then runs `expand_copy` on int8.
`expand_as` is not in that list, and it only decomposes to `expand` at
`to_edge`, after quantization, so every `expand_as` broadcast stayed in float
behind a dequantize and a requantize.

`ExpandAsToExpandPass` runs in `transform_for_annotation` and rewrites
`expand_as(x, other)` into `expand(x, other.shape)`, emitting `sym_size` nodes
for symbolic dims. `expand_as` then gets the same neighbor-driven treatment as
`expand`: int8 when its neighbors are quantized, float otherwise. The shape-only
`other` operand is no longer an input of the op, so it is never pulled into the
shared-qparams cluster.

No new pattern check or support-dict entry is added for `expand`, so an expand
between float-only ops is not forced to int8.

Differential Revision: D127380012
pssrawat added a commit to pssrawat/executorch that referenced this pull request Oct 29, 2026
pytorch#24153)

Summary:

`SharedQspecQuantizer` already gives `expand` and `expand_copy` shared qparams
when a neighbor is quantized, and the lowering then runs `expand_copy` on int8.
`expand_as` is not in that list, and it only decomposes to `expand` at
`to_edge`, after quantization, so every `expand_as` broadcast stayed in float
behind a dequantize and a requantize.

`ExpandAsToExpandPass` runs in `transform_for_annotation` and rewrites
`expand_as(x, other)` into `expand(x, other.shape)`, emitting `sym_size` nodes
for symbolic dims. `expand_as` then gets the same neighbor-driven treatment as
`expand`: int8 when its neighbors are quantized, float otherwise. The shape-only
`other` operand is no longer an input of the op, so it is never pulled into the
shared-qparams cluster.

No new pattern check or support-dict entry is added for `expand`, so an expand
between float-only ops is not forced to int8.

Differential Revision: D127380012
@meta-codesync meta-codesync Bot changed the title Quantize expand_as through the existing shared-qparams expand path (#24153) Keep the shape operand of view_as and expand_as out of shared qparams (#24153) Oct 30, 2026
pssrawat added a commit to pssrawat/executorch that referenced this pull request Oct 30, 2026
…pytorch#24153)

Summary:

`SharedQspecQuantizer` gives each shared-qparams cluster one set of qparams for every float input and output of its ops. `view_as(x, other)` is in that list, and the cluster was built from all of its inputs, so `other` was pulled in even though it contributes only its shape. `x` and `other` were then quantized with one scale, set by whichever has the wider range: the data loses precision, or the shape operand is clipped.

For example, in `(x * y).view_as(other) + other` with `other = z + z`, `x * y` spans about ±2.5 and `other` about ±51. `x * y` was quantized at `other`'s scale of 0.349, about 18x coarser than the 0.0195 it gets when the same reshape is written `reshape(4, 8)`, and the output error against float rose from 0.258 to 0.307.

`expand_as` has the same shape-only second operand but was not in the shared-qparams list at all, so every `expand_as` broadcast stayed in float behind a dequantize and a requantize.

Shape-as ops (`view_as`, `expand_as`) now share qparams only through their data operand, `args[0]`. The cluster walk does not cross the edge to the shape operand in either direction, and that operand gets no shared input qspec, so it keeps the qparams of its own producer. `expand_as` joins the shared-qparams ops, so like `expand` it is quantized to int8 when its neighbours are, and lowers on Cortex-M to an int8 `expand_copy`.

Authored with Claude Code.

Differential Revision: D127380012
@pssrawat
pssrawat requested a review from digantdesai as a code owner October 30, 2026 05:48
@github-actions github-actions Bot added ciflow/trunk module: arm Issues related to arm backend labels Oct 30, 2026
pssrawat added a commit to pssrawat/executorch that referenced this pull request Oct 30, 2026
…pytorch#24153)

Summary:

`SharedQspecQuantizer` gives each shared-qparams cluster one set of qparams for every float input and output of its ops. `view_as(x, other)` is in that list, and the cluster was built from all of its inputs, so `other` was pulled in even though it contributes only its shape. `x` and `other` were then quantized with one scale, set by whichever has the wider range: the data loses precision, or the shape operand is clipped.

For example, in `(x * y).view_as(other) + other` with `other = z + z`, `x * y` spans about ±2.5 and `other` about ±51. `x * y` was quantized at `other`'s scale of 0.349, about 18x coarser than the 0.0195 it gets when the same reshape is written `reshape(4, 8)`, and the output error against float rose from 0.258 to 0.307.

`expand_as` has the same shape-only second operand but was not in the shared-qparams list at all, so every `expand_as` broadcast stayed in float behind a dequantize and a requantize.

Shape-as ops (`view_as`, `expand_as`) now share qparams only through their data operand, `args[0]`. The cluster walk does not cross the edge to the shape operand in either direction, and that operand gets no shared input qspec, so it keeps the qparams of its own producer. `expand_as` joins the shared-qparams ops, so like `expand` it is quantized to int8 when its neighbours are, and lowers on Cortex-M to an int8 `expand_copy`.

Authored with Claude Code.

Differential Revision: D127380012
Summary:

Q/DQ folding (`FoldAndAnnotateQParamsPass`) always folds the quantize/dequantize pair around a constant-producing op (`full`, `full_like`, `arange`, `eye`, `linspace`, `scalar_tensor`) into the op: it removes the Q node and records the output qparams, but the op itself keeps producing float. The Arm backend follows folding with `ComputeConstantOpsAOTPass`, which evaluates such ops in float and quantizes the result. Cortex-M did not, so in `x + torch.full(shape, 0.5)` the int8 `quantized_add` kernel read a float32 constant, and the output was silently wrong.

Add `ComputeQuantizedConstantOpsPass` and run it after folding on both Cortex-M pass lists.

- a. It only touches nodes with output qparams, so unquantized constant ops are unchanged. A quantized `full` stays a runtime fill, with its fill value quantized and its dtype set to the qparams' dtype, so a large fill does not turn into a large constant;
- b.`full_like` is first converted to `full` with the Arm backend's `ConvertFullLikeToFullPass`.

This also fixes a test that was already failing. `surrounded_quantized_op_constant` in `misc/test_quantization.py` was marked as an expected failure. Its `torch.ones(2, 2)` constant is lowered to a `full` op, so the int8 add read float data, and every output element was off by about 0.49. With this change the case matches the quantized reference exactly, so its `xfail` is removed. The other constants in that file are stored tensors, which folding already quantized correctly.

Differential Revision: D128002647
…pytorch#24153)

Summary:

`SharedQspecQuantizer` gives each shared-qparams cluster one set of qparams for every float input and output of its ops. `view_as(x, other)` is in that list, and the cluster was built from all of its inputs, so `other` was pulled in even though it contributes only its shape. `x` and `other` were then quantized with one scale, set by whichever has the wider range: the data loses precision, or the shape operand is clipped.

For example, in `(x * y).view_as(other) + other` with `other = z + z`, `x * y` spans about ±2.5 and `other` about ±51. `x * y` was quantized at `other`'s scale of 0.349, about 18x coarser than the 0.0195 it gets when the same reshape is written `reshape(4, 8)`, and the output error against float rose from 0.258 to 0.307.

`expand_as` has the same shape-only second operand but was not in the shared-qparams list at all, so every `expand_as` broadcast stayed in float behind a dequantize and a requantize.

Shape-as ops (`view_as`, `expand_as`) now share qparams only through their data operand, `args[0]`. The cluster walk does not cross the edge to the shape operand in either direction, and that operand gets no shared input qspec, so it keeps the qparams of its own producer. `expand_as` joins the shared-qparams ops, so like `expand` it is quantized to int8 when its neighbours are, and lowers on Cortex-M to an int8 `expand_copy`.

Authored with Claude Code.

Differential Revision: D127380012
pssrawat added a commit to pssrawat/executorch that referenced this pull request Oct 30, 2026
…pytorch#24153)

Summary:

`SharedQspecQuantizer` gives each shared-qparams cluster one set of qparams for every float input and output of its ops. `view_as(x, other)` is in that list, and the cluster was built from all of its inputs, so `other` was pulled in even though it contributes only its shape. `x` and `other` were then quantized with one scale, set by whichever has the wider range: the data loses precision, or the shape operand is clipped.

For example, in `(x * y).view_as(other) + other` with `other = z + z`, `x * y` spans about ±2.5 and `other` about ±51. `x * y` was quantized at `other`'s scale of 0.349, about 18x coarser than the 0.0195 it gets when the same reshape is written `reshape(4, 8)`, and the output error against float rose from 0.258 to 0.307.

`expand_as` has the same shape-only second operand but was not in the shared-qparams list at all, so every `expand_as` broadcast stayed in float behind a dequantize and a requantize.

Shape-as ops (`view_as`, `expand_as`) now share qparams only through their data operand, `args[0]`. The cluster walk does not cross the edge to the shape operand in either direction, and that operand gets no shared input qspec, so it keeps the qparams of its own producer. `expand_as` joins the shared-qparams ops, so like `expand` it is quantized to int8 when its neighbours are, and lowers on Cortex-M to an int8 `expand_copy`.

Authored with Claude Code.

Differential Revision: D127380012

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

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 release notes: none Do not include this in the release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant