Skip to content

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

Merged
meta-codesync[bot] merged 1 commit into
pytorch:mainfrom
pssrawat:export-D127380012
Nov 3, 2026
Merged

meta-codesync[bot] merged 1 commit 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.

Reviewed By: rascani

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.

✅ You can merge normally! (1 Unrelated Failure)

As of commit 85c948a with merge base 893d815 (image):

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.

@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
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 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
@rascani
rascani requested a review from AdrianLundell November 2, 2026 16:31

@rascani rascani 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.

Review automatically exported from Phabricator review in Meta.

pssrawat added a commit to pssrawat/executorch that referenced this pull request Nov 2, 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.

Reviewed By: rascani

Differential Revision: D127380012
pssrawat added a commit to pssrawat/executorch that referenced this pull request Nov 2, 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.

Reviewed By: rascani

Differential Revision: D127380012
…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.

Reviewed By: rascani

Differential Revision: D127380012
pssrawat added a commit to pssrawat/executorch that referenced this pull request Nov 2, 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.

Reviewed By: rascani

Differential Revision: D127380012
@meta-codesync
meta-codesync Bot merged commit 0f8e096 into pytorch:main Nov 3, 2026
618 of 623 checks passed
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.

2 participants