Fix double-tracing in SpecPropPass - #15485

Merged
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581
Dec 8, 2025
Merged

Fix double-tracing in SpecPropPass#15485
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581

Conversation

@GregoryComer

@GregoryComerGregoryComer commented Oct 31, 2025

Copy link
Copy Markdown
Contributor

Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:

  • Every time we trace through the graph, we generate new symints.
  • That's fine, since shape_env will pick up guards during the retrace.
  • Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) (https://github.com/.../exir/passes/spec_prop_pass.py...).
  • The tensor spec gets the symint from the first. But the graph and guards use the second.
  • Hence the tensor spec doesn't pick up on guards.

To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks @angelayi for the suggestion). This resolves the issue.

I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.

Differential Revision: D85913581

@pytorch-bot

pytorch-botBot commented Oct 31, 2025

Copy link
Copy Markdown

🔗 Helpful Links

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

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 7da6e25 with merge base 18c1c5b (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-clameta-claBot 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 31, 2025
@meta-codesync

Copy link
Copy Markdown
Contributor

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

@GregoryComer
GregoryComer marked this pull request as draft October 31, 2025 00:24
@GregoryComerGregoryComer added ciflow/trunk release notes: exir Changes to any dialects and passes on these dialects, such as memory planning labels Oct 31, 2025
pytorch-botBot pushed a commit that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: #15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

One additional note is that aliasing analysis feels pretty fragile as is. I fixed several subtle issues (luckily caught by CI) where my changes were accidentally planning two seperate tensors when they should alias / share one TensorSpec.

I'm wondering if we should re-write this pass again to either rely on ProxyValue reference equality or otherwise introduce some proper aliasing analysis. This is as opposed to hard coding that getitem and output, for example, always alias their argument.

This seems like it could get messy with non-functional custom ops or defunctionalization, in general. @JacobSzwejbka@angelayi what are your thoughts on this?

@GregoryComerGregoryComer changed the title Fix shape_env handling in SpecPropPassFix double-tracing in SpecPropPassNov 7, 2025
Comment threadexir/passes/spec_prop_pass.py Outdated
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 8, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 10, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 11, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@meta-codesync

Copy link
Copy Markdown
Contributor

@GregoryComer has imported this pull request. If you are a Meta employee, you can view this in D85913581.

@GregoryComer
GregoryComer marked this pull request as ready for review November 11, 2025 05:10
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

Note that the moshi and zephyr size test failures are pre-existing.

CopilotAI 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.

Pull request overview

This PR fixes a double-tracing issue in SpecPropPass where tensor specs were generated with symints from a different trace than the one used for guards, causing guards on unbacked symints to be lost. The fix refactors SpecPropPass to perform a single re-trace using the parent ExportPass class and then generate specs from the resulting metadata, ensuring consistency between specs and guards.

Key changes:

  • Rewrote SpecPropPass.__call__() to re-trace once and populate specs from meta values
  • Removed individual node handler methods (placeholder, call_operator, call_getitem, etc.) in favor of unified spec generation
  • Added test case with custom op using unbacked symints to verify guard propagation

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
exir/passes/spec_prop_pass.pyComplete rewrite of SpecPropPass to use single re-trace strategy; replaces per-node callbacks with post-trace spec generation from meta values
exir/tests/test_passes.pyAdds custom ops (unbacked, unbacked.out) and test case to verify spec propagation correctly captures guards for unbacked symints

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/tests/test_passes.py Outdated
Comment on lines +83 to +86
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

Consider merging these 2 conditions?

@GregoryComerGregoryComerDec 4, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

There is some weird existing behavior here that seems to need to be preserved (barring a larger update). Basically, we don't want to regenerate call_delegate node specs but do want to regenerate everything else. I'll add a comment detailing why.

Comment on lines -107 to -125
def call_cond(self, pred, true_fn, false_fn, inputs, meta):
# true_fn/false_fn return tensors of the same shape, so we can pick
# either one here.
*_, true_out_node = true_fn.graph.nodes
meta["spec"] = pytree.tree_map(make_spec, true_out_node.meta["val"])
return super().call_cond(pred, true_fn, false_fn, inputs, meta)

def call_while(
self,
cond_fn: torch.fx.GraphModule,
body_fn: torch.fx.GraphModule,
carried_inputs: List[ProxyValue],
additional_inputs: List[ProxyValue],
meta: NodeMetadata,
):
meta["spec"] = pytree.tree_map(make_spec, carried_inputs)
return super().call_while(
cond_fn, body_fn, carried_inputs, additional_inputs, meta
)

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.

Why we don't have to handle condition and while anymore?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They should be handled by having the tracing logic use ExportPass to regenerate the meta values and then assigning spec values for each node correspondingly. I did go ahead and specific tests for cond and while to verify that the specs are generated correctly. As long as the cond + while outputs don't alias anything else (my understanding is that this should be the case), it should be good.

GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
Comment on lines +79 to +94
elif (
node.op == "call_function"
and node.target == executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

I meant something like:

Suggested change
elif (
node.op=="call_function"
andnode.target==executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure - the issue that I've seen is that sometimes this pass gets called multiple times (Cadence backend does this, for example) and thus we need to regenerate the spec for most nodes to make sure they pick up on any shape changes between calls.

But if we regenerate the spec for call_delegate nodes, it breaks things. So the if "spec" not in node.meta: condition should only apply to call_delegate but not anything else. Otherwise is breaks existing backend assumptions.

Ideally, we'll do a deeper change to fix this but this preserves the existing behavior. I could change the line to if "spec" not in node.meta or node.target != executorch_call_delegate if you'd prefer.

Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
@GregoryComer
GregoryComer merged commit 3ad2610 into pytorch:mainDec 8, 2025
299 of 302 checks passed
jirioc pushed a commit to nxp-upstream/executorch that referenced this pull request Dec 19, 2025
Our current SpecPropPass doesn't properly capture the effect of guards
in the shape environment due to double-tracing certain ops. The problem
looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the
spec and then once by calling super().call_operator(...)
([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and
guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and
then generate specs based on the meta values, not the traced ProxyValues
(thanks @angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid
adding a new dep to the core EXIR tests, I've written a test with a
custom op that uses an unbacked symint in the meta kernel output shape
to replicate the bug in the same way.
Differential Revision: D85913581
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exportedrelease notes: exirChanges to any dialects and passes on these dialects, such as memory planning

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GregoryComer@larryliu0820@angelayi
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix double-tracing in SpecPropPass - #15485

Merged
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581
Dec 8, 2025
Merged

Fix double-tracing in SpecPropPass#15485
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581

Conversation

@GregoryComer

@GregoryComerGregoryComer commented Oct 31, 2025

Copy link
Copy Markdown
Contributor

Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:

  • Every time we trace through the graph, we generate new symints.
  • That's fine, since shape_env will pick up guards during the retrace.
  • Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) (https://github.com/.../exir/passes/spec_prop_pass.py...).
  • The tensor spec gets the symint from the first. But the graph and guards use the second.
  • Hence the tensor spec doesn't pick up on guards.

To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks @angelayi for the suggestion). This resolves the issue.

I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.

Differential Revision: D85913581

@pytorch-bot

pytorch-botBot commented Oct 31, 2025

Copy link
Copy Markdown

🔗 Helpful Links

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

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 7da6e25 with merge base 18c1c5b (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-clameta-claBot 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 31, 2025
@meta-codesync

Copy link
Copy Markdown
Contributor

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

@GregoryComer
GregoryComer marked this pull request as draft October 31, 2025 00:24
@GregoryComerGregoryComer added ciflow/trunk release notes: exir Changes to any dialects and passes on these dialects, such as memory planning labels Oct 31, 2025
pytorch-botBot pushed a commit that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: #15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

One additional note is that aliasing analysis feels pretty fragile as is. I fixed several subtle issues (luckily caught by CI) where my changes were accidentally planning two seperate tensors when they should alias / share one TensorSpec.

I'm wondering if we should re-write this pass again to either rely on ProxyValue reference equality or otherwise introduce some proper aliasing analysis. This is as opposed to hard coding that getitem and output, for example, always alias their argument.

This seems like it could get messy with non-functional custom ops or defunctionalization, in general. @JacobSzwejbka@angelayi what are your thoughts on this?

@GregoryComerGregoryComer changed the title Fix shape_env handling in SpecPropPassFix double-tracing in SpecPropPassNov 7, 2025
Comment threadexir/passes/spec_prop_pass.py Outdated
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 8, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 10, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 11, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@meta-codesync

Copy link
Copy Markdown
Contributor

@GregoryComer has imported this pull request. If you are a Meta employee, you can view this in D85913581.

@GregoryComer
GregoryComer marked this pull request as ready for review November 11, 2025 05:10
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

Note that the moshi and zephyr size test failures are pre-existing.

CopilotAI 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.

Pull request overview

This PR fixes a double-tracing issue in SpecPropPass where tensor specs were generated with symints from a different trace than the one used for guards, causing guards on unbacked symints to be lost. The fix refactors SpecPropPass to perform a single re-trace using the parent ExportPass class and then generate specs from the resulting metadata, ensuring consistency between specs and guards.

Key changes:

  • Rewrote SpecPropPass.__call__() to re-trace once and populate specs from meta values
  • Removed individual node handler methods (placeholder, call_operator, call_getitem, etc.) in favor of unified spec generation
  • Added test case with custom op using unbacked symints to verify guard propagation

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
exir/passes/spec_prop_pass.pyComplete rewrite of SpecPropPass to use single re-trace strategy; replaces per-node callbacks with post-trace spec generation from meta values
exir/tests/test_passes.pyAdds custom ops (unbacked, unbacked.out) and test case to verify spec propagation correctly captures guards for unbacked symints

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/tests/test_passes.py Outdated
Comment on lines +83 to +86
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

Consider merging these 2 conditions?

@GregoryComerGregoryComerDec 4, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

There is some weird existing behavior here that seems to need to be preserved (barring a larger update). Basically, we don't want to regenerate call_delegate node specs but do want to regenerate everything else. I'll add a comment detailing why.

Comment on lines -107 to -125
def call_cond(self, pred, true_fn, false_fn, inputs, meta):
# true_fn/false_fn return tensors of the same shape, so we can pick
# either one here.
*_, true_out_node = true_fn.graph.nodes
meta["spec"] = pytree.tree_map(make_spec, true_out_node.meta["val"])
return super().call_cond(pred, true_fn, false_fn, inputs, meta)

def call_while(
self,
cond_fn: torch.fx.GraphModule,
body_fn: torch.fx.GraphModule,
carried_inputs: List[ProxyValue],
additional_inputs: List[ProxyValue],
meta: NodeMetadata,
):
meta["spec"] = pytree.tree_map(make_spec, carried_inputs)
return super().call_while(
cond_fn, body_fn, carried_inputs, additional_inputs, meta
)

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.

Why we don't have to handle condition and while anymore?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They should be handled by having the tracing logic use ExportPass to regenerate the meta values and then assigning spec values for each node correspondingly. I did go ahead and specific tests for cond and while to verify that the specs are generated correctly. As long as the cond + while outputs don't alias anything else (my understanding is that this should be the case), it should be good.

GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
Comment on lines +79 to +94
elif (
node.op == "call_function"
and node.target == executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

I meant something like:

Suggested change
elif (
node.op=="call_function"
andnode.target==executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure - the issue that I've seen is that sometimes this pass gets called multiple times (Cadence backend does this, for example) and thus we need to regenerate the spec for most nodes to make sure they pick up on any shape changes between calls.

But if we regenerate the spec for call_delegate nodes, it breaks things. So the if "spec" not in node.meta: condition should only apply to call_delegate but not anything else. Otherwise is breaks existing backend assumptions.

Ideally, we'll do a deeper change to fix this but this preserves the existing behavior. I could change the line to if "spec" not in node.meta or node.target != executorch_call_delegate if you'd prefer.

Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
@GregoryComer
GregoryComer merged commit 3ad2610 into pytorch:mainDec 8, 2025
299 of 302 checks passed
jirioc pushed a commit to nxp-upstream/executorch that referenced this pull request Dec 19, 2025
Our current SpecPropPass doesn't properly capture the effect of guards
in the shape environment due to double-tracing certain ops. The problem
looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the
spec and then once by calling super().call_operator(...)
([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and
guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and
then generate specs based on the meta values, not the traced ProxyValues
(thanks @angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid
adding a new dep to the core EXIR tests, I've written a test with a
custom op that uses an unbacked symint in the meta kernel output shape
to replicate the bug in the same way.
Differential Revision: D85913581
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exportedrelease notes: exirChanges to any dialects and passes on these dialects, such as memory planning

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GregoryComer@larryliu0820@angelayi
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix double-tracing in SpecPropPass - #15485

Merged
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581
Dec 8, 2025
Merged

Fix double-tracing in SpecPropPass#15485
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581

Conversation

@GregoryComer

@GregoryComerGregoryComer commented Oct 31, 2025

Copy link
Copy Markdown
Contributor

Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:

  • Every time we trace through the graph, we generate new symints.
  • That's fine, since shape_env will pick up guards during the retrace.
  • Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) (https://github.com/.../exir/passes/spec_prop_pass.py...).
  • The tensor spec gets the symint from the first. But the graph and guards use the second.
  • Hence the tensor spec doesn't pick up on guards.

To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks @angelayi for the suggestion). This resolves the issue.

I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.

Differential Revision: D85913581

@pytorch-bot

pytorch-botBot commented Oct 31, 2025

Copy link
Copy Markdown

🔗 Helpful Links

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

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 7da6e25 with merge base 18c1c5b (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-clameta-claBot 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 31, 2025
@meta-codesync

Copy link
Copy Markdown
Contributor

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

@GregoryComer
GregoryComer marked this pull request as draft October 31, 2025 00:24
@GregoryComerGregoryComer added ciflow/trunk release notes: exir Changes to any dialects and passes on these dialects, such as memory planning labels Oct 31, 2025
pytorch-botBot pushed a commit that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: #15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

One additional note is that aliasing analysis feels pretty fragile as is. I fixed several subtle issues (luckily caught by CI) where my changes were accidentally planning two seperate tensors when they should alias / share one TensorSpec.

I'm wondering if we should re-write this pass again to either rely on ProxyValue reference equality or otherwise introduce some proper aliasing analysis. This is as opposed to hard coding that getitem and output, for example, always alias their argument.

This seems like it could get messy with non-functional custom ops or defunctionalization, in general. @JacobSzwejbka@angelayi what are your thoughts on this?

@GregoryComerGregoryComer changed the title Fix shape_env handling in SpecPropPassFix double-tracing in SpecPropPassNov 7, 2025
Comment threadexir/passes/spec_prop_pass.py Outdated
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 8, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 10, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 11, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@meta-codesync

Copy link
Copy Markdown
Contributor

@GregoryComer has imported this pull request. If you are a Meta employee, you can view this in D85913581.

@GregoryComer
GregoryComer marked this pull request as ready for review November 11, 2025 05:10
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

Note that the moshi and zephyr size test failures are pre-existing.

CopilotAI 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.

Pull request overview

This PR fixes a double-tracing issue in SpecPropPass where tensor specs were generated with symints from a different trace than the one used for guards, causing guards on unbacked symints to be lost. The fix refactors SpecPropPass to perform a single re-trace using the parent ExportPass class and then generate specs from the resulting metadata, ensuring consistency between specs and guards.

Key changes:

  • Rewrote SpecPropPass.__call__() to re-trace once and populate specs from meta values
  • Removed individual node handler methods (placeholder, call_operator, call_getitem, etc.) in favor of unified spec generation
  • Added test case with custom op using unbacked symints to verify guard propagation

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
exir/passes/spec_prop_pass.pyComplete rewrite of SpecPropPass to use single re-trace strategy; replaces per-node callbacks with post-trace spec generation from meta values
exir/tests/test_passes.pyAdds custom ops (unbacked, unbacked.out) and test case to verify spec propagation correctly captures guards for unbacked symints

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/tests/test_passes.py Outdated
Comment on lines +83 to +86
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

Consider merging these 2 conditions?

@GregoryComerGregoryComerDec 4, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

There is some weird existing behavior here that seems to need to be preserved (barring a larger update). Basically, we don't want to regenerate call_delegate node specs but do want to regenerate everything else. I'll add a comment detailing why.

Comment on lines -107 to -125
def call_cond(self, pred, true_fn, false_fn, inputs, meta):
# true_fn/false_fn return tensors of the same shape, so we can pick
# either one here.
*_, true_out_node = true_fn.graph.nodes
meta["spec"] = pytree.tree_map(make_spec, true_out_node.meta["val"])
return super().call_cond(pred, true_fn, false_fn, inputs, meta)

def call_while(
self,
cond_fn: torch.fx.GraphModule,
body_fn: torch.fx.GraphModule,
carried_inputs: List[ProxyValue],
additional_inputs: List[ProxyValue],
meta: NodeMetadata,
):
meta["spec"] = pytree.tree_map(make_spec, carried_inputs)
return super().call_while(
cond_fn, body_fn, carried_inputs, additional_inputs, meta
)

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.

Why we don't have to handle condition and while anymore?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They should be handled by having the tracing logic use ExportPass to regenerate the meta values and then assigning spec values for each node correspondingly. I did go ahead and specific tests for cond and while to verify that the specs are generated correctly. As long as the cond + while outputs don't alias anything else (my understanding is that this should be the case), it should be good.

GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
Comment on lines +79 to +94
elif (
node.op == "call_function"
and node.target == executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

I meant something like:

Suggested change
elif (
node.op=="call_function"
andnode.target==executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure - the issue that I've seen is that sometimes this pass gets called multiple times (Cadence backend does this, for example) and thus we need to regenerate the spec for most nodes to make sure they pick up on any shape changes between calls.

But if we regenerate the spec for call_delegate nodes, it breaks things. So the if "spec" not in node.meta: condition should only apply to call_delegate but not anything else. Otherwise is breaks existing backend assumptions.

Ideally, we'll do a deeper change to fix this but this preserves the existing behavior. I could change the line to if "spec" not in node.meta or node.target != executorch_call_delegate if you'd prefer.

Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
@GregoryComer
GregoryComer merged commit 3ad2610 into pytorch:mainDec 8, 2025
299 of 302 checks passed
jirioc pushed a commit to nxp-upstream/executorch that referenced this pull request Dec 19, 2025
Our current SpecPropPass doesn't properly capture the effect of guards
in the shape environment due to double-tracing certain ops. The problem
looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the
spec and then once by calling super().call_operator(...)
([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and
guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and
then generate specs based on the meta values, not the traced ProxyValues
(thanks @angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid
adding a new dep to the core EXIR tests, I've written a test with a
custom op that uses an unbacked symint in the meta kernel output shape
to replicate the bug in the same way.
Differential Revision: D85913581
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exportedrelease notes: exirChanges to any dialects and passes on these dialects, such as memory planning

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GregoryComer@larryliu0820@angelayi
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix double-tracing in SpecPropPass - #15485

Merged
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581
Dec 8, 2025
Merged

Fix double-tracing in SpecPropPass#15485
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581

Conversation

@GregoryComer

@GregoryComerGregoryComer commented Oct 31, 2025

Copy link
Copy Markdown
Contributor

Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:

  • Every time we trace through the graph, we generate new symints.
  • That's fine, since shape_env will pick up guards during the retrace.
  • Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) (https://github.com/.../exir/passes/spec_prop_pass.py...).
  • The tensor spec gets the symint from the first. But the graph and guards use the second.
  • Hence the tensor spec doesn't pick up on guards.

To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks @angelayi for the suggestion). This resolves the issue.

I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.

Differential Revision: D85913581

@pytorch-bot

pytorch-botBot commented Oct 31, 2025

Copy link
Copy Markdown

🔗 Helpful Links

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

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 7da6e25 with merge base 18c1c5b (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-clameta-claBot 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 31, 2025
@meta-codesync

Copy link
Copy Markdown
Contributor

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

@GregoryComer
GregoryComer marked this pull request as draft October 31, 2025 00:24
@GregoryComerGregoryComer added ciflow/trunk release notes: exir Changes to any dialects and passes on these dialects, such as memory planning labels Oct 31, 2025
pytorch-botBot pushed a commit that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: #15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

One additional note is that aliasing analysis feels pretty fragile as is. I fixed several subtle issues (luckily caught by CI) where my changes were accidentally planning two seperate tensors when they should alias / share one TensorSpec.

I'm wondering if we should re-write this pass again to either rely on ProxyValue reference equality or otherwise introduce some proper aliasing analysis. This is as opposed to hard coding that getitem and output, for example, always alias their argument.

This seems like it could get messy with non-functional custom ops or defunctionalization, in general. @JacobSzwejbka@angelayi what are your thoughts on this?

@GregoryComerGregoryComer changed the title Fix shape_env handling in SpecPropPassFix double-tracing in SpecPropPassNov 7, 2025
Comment threadexir/passes/spec_prop_pass.py Outdated
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 8, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 10, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 11, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@meta-codesync

Copy link
Copy Markdown
Contributor

@GregoryComer has imported this pull request. If you are a Meta employee, you can view this in D85913581.

@GregoryComer
GregoryComer marked this pull request as ready for review November 11, 2025 05:10
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

Note that the moshi and zephyr size test failures are pre-existing.

CopilotAI 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.

Pull request overview

This PR fixes a double-tracing issue in SpecPropPass where tensor specs were generated with symints from a different trace than the one used for guards, causing guards on unbacked symints to be lost. The fix refactors SpecPropPass to perform a single re-trace using the parent ExportPass class and then generate specs from the resulting metadata, ensuring consistency between specs and guards.

Key changes:

  • Rewrote SpecPropPass.__call__() to re-trace once and populate specs from meta values
  • Removed individual node handler methods (placeholder, call_operator, call_getitem, etc.) in favor of unified spec generation
  • Added test case with custom op using unbacked symints to verify guard propagation

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
exir/passes/spec_prop_pass.pyComplete rewrite of SpecPropPass to use single re-trace strategy; replaces per-node callbacks with post-trace spec generation from meta values
exir/tests/test_passes.pyAdds custom ops (unbacked, unbacked.out) and test case to verify spec propagation correctly captures guards for unbacked symints

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/tests/test_passes.py Outdated
Comment on lines +83 to +86
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

Consider merging these 2 conditions?

@GregoryComerGregoryComerDec 4, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

There is some weird existing behavior here that seems to need to be preserved (barring a larger update). Basically, we don't want to regenerate call_delegate node specs but do want to regenerate everything else. I'll add a comment detailing why.

Comment on lines -107 to -125
def call_cond(self, pred, true_fn, false_fn, inputs, meta):
# true_fn/false_fn return tensors of the same shape, so we can pick
# either one here.
*_, true_out_node = true_fn.graph.nodes
meta["spec"] = pytree.tree_map(make_spec, true_out_node.meta["val"])
return super().call_cond(pred, true_fn, false_fn, inputs, meta)

def call_while(
self,
cond_fn: torch.fx.GraphModule,
body_fn: torch.fx.GraphModule,
carried_inputs: List[ProxyValue],
additional_inputs: List[ProxyValue],
meta: NodeMetadata,
):
meta["spec"] = pytree.tree_map(make_spec, carried_inputs)
return super().call_while(
cond_fn, body_fn, carried_inputs, additional_inputs, meta
)

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.

Why we don't have to handle condition and while anymore?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They should be handled by having the tracing logic use ExportPass to regenerate the meta values and then assigning spec values for each node correspondingly. I did go ahead and specific tests for cond and while to verify that the specs are generated correctly. As long as the cond + while outputs don't alias anything else (my understanding is that this should be the case), it should be good.

GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
Comment on lines +79 to +94
elif (
node.op == "call_function"
and node.target == executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

I meant something like:

Suggested change
elif (
node.op=="call_function"
andnode.target==executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure - the issue that I've seen is that sometimes this pass gets called multiple times (Cadence backend does this, for example) and thus we need to regenerate the spec for most nodes to make sure they pick up on any shape changes between calls.

But if we regenerate the spec for call_delegate nodes, it breaks things. So the if "spec" not in node.meta: condition should only apply to call_delegate but not anything else. Otherwise is breaks existing backend assumptions.

Ideally, we'll do a deeper change to fix this but this preserves the existing behavior. I could change the line to if "spec" not in node.meta or node.target != executorch_call_delegate if you'd prefer.

Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
@GregoryComer
GregoryComer merged commit 3ad2610 into pytorch:mainDec 8, 2025
299 of 302 checks passed
jirioc pushed a commit to nxp-upstream/executorch that referenced this pull request Dec 19, 2025
Our current SpecPropPass doesn't properly capture the effect of guards
in the shape environment due to double-tracing certain ops. The problem
looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the
spec and then once by calling super().call_operator(...)
([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and
guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and
then generate specs based on the meta values, not the traced ProxyValues
(thanks @angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid
adding a new dep to the core EXIR tests, I've written a test with a
custom op that uses an unbacked symint in the meta kernel output shape
to replicate the bug in the same way.
Differential Revision: D85913581
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exportedrelease notes: exirChanges to any dialects and passes on these dialects, such as memory planning

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GregoryComer@larryliu0820@angelayi
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix double-tracing in SpecPropPass - #15485

Merged
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581
Dec 8, 2025
Merged

Fix double-tracing in SpecPropPass#15485
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581

Conversation

@GregoryComer

@GregoryComerGregoryComer commented Oct 31, 2025

Copy link
Copy Markdown
Contributor

Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:

  • Every time we trace through the graph, we generate new symints.
  • That's fine, since shape_env will pick up guards during the retrace.
  • Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) (https://github.com/.../exir/passes/spec_prop_pass.py...).
  • The tensor spec gets the symint from the first. But the graph and guards use the second.
  • Hence the tensor spec doesn't pick up on guards.

To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks @angelayi for the suggestion). This resolves the issue.

I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.

Differential Revision: D85913581

@pytorch-bot

pytorch-botBot commented Oct 31, 2025

Copy link
Copy Markdown

🔗 Helpful Links

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

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 7da6e25 with merge base 18c1c5b (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-clameta-claBot 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 31, 2025
@meta-codesync

Copy link
Copy Markdown
Contributor

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

@GregoryComer
GregoryComer marked this pull request as draft October 31, 2025 00:24
@GregoryComerGregoryComer added ciflow/trunk release notes: exir Changes to any dialects and passes on these dialects, such as memory planning labels Oct 31, 2025
pytorch-botBot pushed a commit that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: #15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

One additional note is that aliasing analysis feels pretty fragile as is. I fixed several subtle issues (luckily caught by CI) where my changes were accidentally planning two seperate tensors when they should alias / share one TensorSpec.

I'm wondering if we should re-write this pass again to either rely on ProxyValue reference equality or otherwise introduce some proper aliasing analysis. This is as opposed to hard coding that getitem and output, for example, always alias their argument.

This seems like it could get messy with non-functional custom ops or defunctionalization, in general. @JacobSzwejbka@angelayi what are your thoughts on this?

@GregoryComerGregoryComer changed the title Fix shape_env handling in SpecPropPassFix double-tracing in SpecPropPassNov 7, 2025
Comment threadexir/passes/spec_prop_pass.py Outdated
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 8, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 10, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 11, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@meta-codesync

Copy link
Copy Markdown
Contributor

@GregoryComer has imported this pull request. If you are a Meta employee, you can view this in D85913581.

@GregoryComer
GregoryComer marked this pull request as ready for review November 11, 2025 05:10
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

Note that the moshi and zephyr size test failures are pre-existing.

CopilotAI 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.

Pull request overview

This PR fixes a double-tracing issue in SpecPropPass where tensor specs were generated with symints from a different trace than the one used for guards, causing guards on unbacked symints to be lost. The fix refactors SpecPropPass to perform a single re-trace using the parent ExportPass class and then generate specs from the resulting metadata, ensuring consistency between specs and guards.

Key changes:

  • Rewrote SpecPropPass.__call__() to re-trace once and populate specs from meta values
  • Removed individual node handler methods (placeholder, call_operator, call_getitem, etc.) in favor of unified spec generation
  • Added test case with custom op using unbacked symints to verify guard propagation

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
exir/passes/spec_prop_pass.pyComplete rewrite of SpecPropPass to use single re-trace strategy; replaces per-node callbacks with post-trace spec generation from meta values
exir/tests/test_passes.pyAdds custom ops (unbacked, unbacked.out) and test case to verify spec propagation correctly captures guards for unbacked symints

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/tests/test_passes.py Outdated
Comment on lines +83 to +86
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

Consider merging these 2 conditions?

@GregoryComerGregoryComerDec 4, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

There is some weird existing behavior here that seems to need to be preserved (barring a larger update). Basically, we don't want to regenerate call_delegate node specs but do want to regenerate everything else. I'll add a comment detailing why.

Comment on lines -107 to -125
def call_cond(self, pred, true_fn, false_fn, inputs, meta):
# true_fn/false_fn return tensors of the same shape, so we can pick
# either one here.
*_, true_out_node = true_fn.graph.nodes
meta["spec"] = pytree.tree_map(make_spec, true_out_node.meta["val"])
return super().call_cond(pred, true_fn, false_fn, inputs, meta)

def call_while(
self,
cond_fn: torch.fx.GraphModule,
body_fn: torch.fx.GraphModule,
carried_inputs: List[ProxyValue],
additional_inputs: List[ProxyValue],
meta: NodeMetadata,
):
meta["spec"] = pytree.tree_map(make_spec, carried_inputs)
return super().call_while(
cond_fn, body_fn, carried_inputs, additional_inputs, meta
)

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.

Why we don't have to handle condition and while anymore?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They should be handled by having the tracing logic use ExportPass to regenerate the meta values and then assigning spec values for each node correspondingly. I did go ahead and specific tests for cond and while to verify that the specs are generated correctly. As long as the cond + while outputs don't alias anything else (my understanding is that this should be the case), it should be good.

GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
Comment on lines +79 to +94
elif (
node.op == "call_function"
and node.target == executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

I meant something like:

Suggested change
elif (
node.op=="call_function"
andnode.target==executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure - the issue that I've seen is that sometimes this pass gets called multiple times (Cadence backend does this, for example) and thus we need to regenerate the spec for most nodes to make sure they pick up on any shape changes between calls.

But if we regenerate the spec for call_delegate nodes, it breaks things. So the if "spec" not in node.meta: condition should only apply to call_delegate but not anything else. Otherwise is breaks existing backend assumptions.

Ideally, we'll do a deeper change to fix this but this preserves the existing behavior. I could change the line to if "spec" not in node.meta or node.target != executorch_call_delegate if you'd prefer.

Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
@GregoryComer
GregoryComer merged commit 3ad2610 into pytorch:mainDec 8, 2025
299 of 302 checks passed
jirioc pushed a commit to nxp-upstream/executorch that referenced this pull request Dec 19, 2025
Our current SpecPropPass doesn't properly capture the effect of guards
in the shape environment due to double-tracing certain ops. The problem
looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the
spec and then once by calling super().call_operator(...)
([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and
guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and
then generate specs based on the meta values, not the traced ProxyValues
(thanks @angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid
adding a new dep to the core EXIR tests, I've written a test with a
custom op that uses an unbacked symint in the meta kernel output shape
to replicate the bug in the same way.
Differential Revision: D85913581
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exportedrelease notes: exirChanges to any dialects and passes on these dialects, such as memory planning

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GregoryComer@larryliu0820@angelayi
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix double-tracing in SpecPropPass - #15485

Merged
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581
Dec 8, 2025
Merged

Fix double-tracing in SpecPropPass#15485
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581

Conversation

@GregoryComer

@GregoryComerGregoryComer commented Oct 31, 2025

Copy link
Copy Markdown
Contributor

Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:

  • Every time we trace through the graph, we generate new symints.
  • That's fine, since shape_env will pick up guards during the retrace.
  • Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) (https://github.com/.../exir/passes/spec_prop_pass.py...).
  • The tensor spec gets the symint from the first. But the graph and guards use the second.
  • Hence the tensor spec doesn't pick up on guards.

To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks @angelayi for the suggestion). This resolves the issue.

I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.

Differential Revision: D85913581

@pytorch-bot

pytorch-botBot commented Oct 31, 2025

Copy link
Copy Markdown

🔗 Helpful Links

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

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 7da6e25 with merge base 18c1c5b (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-clameta-claBot 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 31, 2025
@meta-codesync

Copy link
Copy Markdown
Contributor

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

@GregoryComer
GregoryComer marked this pull request as draft October 31, 2025 00:24
@GregoryComerGregoryComer added ciflow/trunk release notes: exir Changes to any dialects and passes on these dialects, such as memory planning labels Oct 31, 2025
pytorch-botBot pushed a commit that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: #15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

One additional note is that aliasing analysis feels pretty fragile as is. I fixed several subtle issues (luckily caught by CI) where my changes were accidentally planning two seperate tensors when they should alias / share one TensorSpec.

I'm wondering if we should re-write this pass again to either rely on ProxyValue reference equality or otherwise introduce some proper aliasing analysis. This is as opposed to hard coding that getitem and output, for example, always alias their argument.

This seems like it could get messy with non-functional custom ops or defunctionalization, in general. @JacobSzwejbka@angelayi what are your thoughts on this?

@GregoryComerGregoryComer changed the title Fix shape_env handling in SpecPropPassFix double-tracing in SpecPropPassNov 7, 2025
Comment threadexir/passes/spec_prop_pass.py Outdated
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 8, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 10, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 11, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@meta-codesync

Copy link
Copy Markdown
Contributor

@GregoryComer has imported this pull request. If you are a Meta employee, you can view this in D85913581.

@GregoryComer
GregoryComer marked this pull request as ready for review November 11, 2025 05:10
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

Note that the moshi and zephyr size test failures are pre-existing.

CopilotAI 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.

Pull request overview

This PR fixes a double-tracing issue in SpecPropPass where tensor specs were generated with symints from a different trace than the one used for guards, causing guards on unbacked symints to be lost. The fix refactors SpecPropPass to perform a single re-trace using the parent ExportPass class and then generate specs from the resulting metadata, ensuring consistency between specs and guards.

Key changes:

  • Rewrote SpecPropPass.__call__() to re-trace once and populate specs from meta values
  • Removed individual node handler methods (placeholder, call_operator, call_getitem, etc.) in favor of unified spec generation
  • Added test case with custom op using unbacked symints to verify guard propagation

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
exir/passes/spec_prop_pass.pyComplete rewrite of SpecPropPass to use single re-trace strategy; replaces per-node callbacks with post-trace spec generation from meta values
exir/tests/test_passes.pyAdds custom ops (unbacked, unbacked.out) and test case to verify spec propagation correctly captures guards for unbacked symints

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/tests/test_passes.py Outdated
Comment on lines +83 to +86
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

Consider merging these 2 conditions?

@GregoryComerGregoryComerDec 4, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

There is some weird existing behavior here that seems to need to be preserved (barring a larger update). Basically, we don't want to regenerate call_delegate node specs but do want to regenerate everything else. I'll add a comment detailing why.

Comment on lines -107 to -125
def call_cond(self, pred, true_fn, false_fn, inputs, meta):
# true_fn/false_fn return tensors of the same shape, so we can pick
# either one here.
*_, true_out_node = true_fn.graph.nodes
meta["spec"] = pytree.tree_map(make_spec, true_out_node.meta["val"])
return super().call_cond(pred, true_fn, false_fn, inputs, meta)

def call_while(
self,
cond_fn: torch.fx.GraphModule,
body_fn: torch.fx.GraphModule,
carried_inputs: List[ProxyValue],
additional_inputs: List[ProxyValue],
meta: NodeMetadata,
):
meta["spec"] = pytree.tree_map(make_spec, carried_inputs)
return super().call_while(
cond_fn, body_fn, carried_inputs, additional_inputs, meta
)

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.

Why we don't have to handle condition and while anymore?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They should be handled by having the tracing logic use ExportPass to regenerate the meta values and then assigning spec values for each node correspondingly. I did go ahead and specific tests for cond and while to verify that the specs are generated correctly. As long as the cond + while outputs don't alias anything else (my understanding is that this should be the case), it should be good.

GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
Comment on lines +79 to +94
elif (
node.op == "call_function"
and node.target == executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

I meant something like:

Suggested change
elif (
node.op=="call_function"
andnode.target==executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure - the issue that I've seen is that sometimes this pass gets called multiple times (Cadence backend does this, for example) and thus we need to regenerate the spec for most nodes to make sure they pick up on any shape changes between calls.

But if we regenerate the spec for call_delegate nodes, it breaks things. So the if "spec" not in node.meta: condition should only apply to call_delegate but not anything else. Otherwise is breaks existing backend assumptions.

Ideally, we'll do a deeper change to fix this but this preserves the existing behavior. I could change the line to if "spec" not in node.meta or node.target != executorch_call_delegate if you'd prefer.

Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
@GregoryComer
GregoryComer merged commit 3ad2610 into pytorch:mainDec 8, 2025
299 of 302 checks passed
jirioc pushed a commit to nxp-upstream/executorch that referenced this pull request Dec 19, 2025
Our current SpecPropPass doesn't properly capture the effect of guards
in the shape environment due to double-tracing certain ops. The problem
looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the
spec and then once by calling super().call_operator(...)
([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and
guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and
then generate specs based on the meta values, not the traced ProxyValues
(thanks @angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid
adding a new dep to the core EXIR tests, I've written a test with a
custom op that uses an unbacked symint in the meta kernel output shape
to replicate the bug in the same way.
Differential Revision: D85913581
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exportedrelease notes: exirChanges to any dialects and passes on these dialects, such as memory planning

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GregoryComer@larryliu0820@angelayi
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix double-tracing in SpecPropPass - #15485

Merged
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581
Dec 8, 2025
Merged

Fix double-tracing in SpecPropPass#15485
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581

Conversation

@GregoryComer

@GregoryComerGregoryComer commented Oct 31, 2025

Copy link
Copy Markdown
Contributor

Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:

  • Every time we trace through the graph, we generate new symints.
  • That's fine, since shape_env will pick up guards during the retrace.
  • Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) (https://github.com/.../exir/passes/spec_prop_pass.py...).
  • The tensor spec gets the symint from the first. But the graph and guards use the second.
  • Hence the tensor spec doesn't pick up on guards.

To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks @angelayi for the suggestion). This resolves the issue.

I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.

Differential Revision: D85913581

@pytorch-bot

pytorch-botBot commented Oct 31, 2025

Copy link
Copy Markdown

🔗 Helpful Links

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

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 7da6e25 with merge base 18c1c5b (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-clameta-claBot 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 31, 2025
@meta-codesync

Copy link
Copy Markdown
Contributor

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

@GregoryComer
GregoryComer marked this pull request as draft October 31, 2025 00:24
@GregoryComerGregoryComer added ciflow/trunk release notes: exir Changes to any dialects and passes on these dialects, such as memory planning labels Oct 31, 2025
pytorch-botBot pushed a commit that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: #15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

One additional note is that aliasing analysis feels pretty fragile as is. I fixed several subtle issues (luckily caught by CI) where my changes were accidentally planning two seperate tensors when they should alias / share one TensorSpec.

I'm wondering if we should re-write this pass again to either rely on ProxyValue reference equality or otherwise introduce some proper aliasing analysis. This is as opposed to hard coding that getitem and output, for example, always alias their argument.

This seems like it could get messy with non-functional custom ops or defunctionalization, in general. @JacobSzwejbka@angelayi what are your thoughts on this?

@GregoryComerGregoryComer changed the title Fix shape_env handling in SpecPropPassFix double-tracing in SpecPropPassNov 7, 2025
Comment threadexir/passes/spec_prop_pass.py Outdated
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 8, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 10, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 11, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@meta-codesync

Copy link
Copy Markdown
Contributor

@GregoryComer has imported this pull request. If you are a Meta employee, you can view this in D85913581.

@GregoryComer
GregoryComer marked this pull request as ready for review November 11, 2025 05:10
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

Note that the moshi and zephyr size test failures are pre-existing.

CopilotAI 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.

Pull request overview

This PR fixes a double-tracing issue in SpecPropPass where tensor specs were generated with symints from a different trace than the one used for guards, causing guards on unbacked symints to be lost. The fix refactors SpecPropPass to perform a single re-trace using the parent ExportPass class and then generate specs from the resulting metadata, ensuring consistency between specs and guards.

Key changes:

  • Rewrote SpecPropPass.__call__() to re-trace once and populate specs from meta values
  • Removed individual node handler methods (placeholder, call_operator, call_getitem, etc.) in favor of unified spec generation
  • Added test case with custom op using unbacked symints to verify guard propagation

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
exir/passes/spec_prop_pass.pyComplete rewrite of SpecPropPass to use single re-trace strategy; replaces per-node callbacks with post-trace spec generation from meta values
exir/tests/test_passes.pyAdds custom ops (unbacked, unbacked.out) and test case to verify spec propagation correctly captures guards for unbacked symints

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/tests/test_passes.py Outdated
Comment on lines +83 to +86
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

Consider merging these 2 conditions?

@GregoryComerGregoryComerDec 4, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

There is some weird existing behavior here that seems to need to be preserved (barring a larger update). Basically, we don't want to regenerate call_delegate node specs but do want to regenerate everything else. I'll add a comment detailing why.

Comment on lines -107 to -125
def call_cond(self, pred, true_fn, false_fn, inputs, meta):
# true_fn/false_fn return tensors of the same shape, so we can pick
# either one here.
*_, true_out_node = true_fn.graph.nodes
meta["spec"] = pytree.tree_map(make_spec, true_out_node.meta["val"])
return super().call_cond(pred, true_fn, false_fn, inputs, meta)

def call_while(
self,
cond_fn: torch.fx.GraphModule,
body_fn: torch.fx.GraphModule,
carried_inputs: List[ProxyValue],
additional_inputs: List[ProxyValue],
meta: NodeMetadata,
):
meta["spec"] = pytree.tree_map(make_spec, carried_inputs)
return super().call_while(
cond_fn, body_fn, carried_inputs, additional_inputs, meta
)

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.

Why we don't have to handle condition and while anymore?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They should be handled by having the tracing logic use ExportPass to regenerate the meta values and then assigning spec values for each node correspondingly. I did go ahead and specific tests for cond and while to verify that the specs are generated correctly. As long as the cond + while outputs don't alias anything else (my understanding is that this should be the case), it should be good.

GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
Comment on lines +79 to +94
elif (
node.op == "call_function"
and node.target == executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

I meant something like:

Suggested change
elif (
node.op=="call_function"
andnode.target==executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure - the issue that I've seen is that sometimes this pass gets called multiple times (Cadence backend does this, for example) and thus we need to regenerate the spec for most nodes to make sure they pick up on any shape changes between calls.

But if we regenerate the spec for call_delegate nodes, it breaks things. So the if "spec" not in node.meta: condition should only apply to call_delegate but not anything else. Otherwise is breaks existing backend assumptions.

Ideally, we'll do a deeper change to fix this but this preserves the existing behavior. I could change the line to if "spec" not in node.meta or node.target != executorch_call_delegate if you'd prefer.

Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
@GregoryComer
GregoryComer merged commit 3ad2610 into pytorch:mainDec 8, 2025
299 of 302 checks passed
jirioc pushed a commit to nxp-upstream/executorch that referenced this pull request Dec 19, 2025
Our current SpecPropPass doesn't properly capture the effect of guards
in the shape environment due to double-tracing certain ops. The problem
looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the
spec and then once by calling super().call_operator(...)
([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and
guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and
then generate specs based on the meta values, not the traced ProxyValues
(thanks @angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid
adding a new dep to the core EXIR tests, I've written a test with a
custom op that uses an unbacked symint in the meta kernel output shape
to replicate the bug in the same way.
Differential Revision: D85913581
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exportedrelease notes: exirChanges to any dialects and passes on these dialects, such as memory planning

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GregoryComer@larryliu0820@angelayi
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix double-tracing in SpecPropPass - #15485

Merged
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581
Dec 8, 2025
Merged

Fix double-tracing in SpecPropPass#15485
GregoryComer merged 1 commit into
pytorch:mainfrom
GregoryComer:export-D85913581

Conversation

@GregoryComer

@GregoryComerGregoryComer commented Oct 31, 2025

Copy link
Copy Markdown
Contributor

Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:

  • Every time we trace through the graph, we generate new symints.
  • That's fine, since shape_env will pick up guards during the retrace.
  • Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) (https://github.com/.../exir/passes/spec_prop_pass.py...).
  • The tensor spec gets the symint from the first. But the graph and guards use the second.
  • Hence the tensor spec doesn't pick up on guards.

To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks @angelayi for the suggestion). This resolves the issue.

I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.

Differential Revision: D85913581

@pytorch-bot

pytorch-botBot commented Oct 31, 2025

Copy link
Copy Markdown

🔗 Helpful Links

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

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 7da6e25 with merge base 18c1c5b (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-clameta-claBot 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 31, 2025
@meta-codesync

Copy link
Copy Markdown
Contributor

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

@GregoryComer
GregoryComer marked this pull request as draft October 31, 2025 00:24
@GregoryComerGregoryComer added ciflow/trunk release notes: exir Changes to any dialects and passes on these dialects, such as memory planning labels Oct 31, 2025
pytorch-botBot pushed a commit that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: #15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 6, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

One additional note is that aliasing analysis feels pretty fragile as is. I fixed several subtle issues (luckily caught by CI) where my changes were accidentally planning two seperate tensors when they should alias / share one TensorSpec.

I'm wondering if we should re-write this pass again to either rely on ProxyValue reference equality or otherwise introduce some proper aliasing analysis. This is as opposed to hard coding that getitem and output, for example, always alias their argument.

This seems like it could get messy with non-functional custom ops or defunctionalization, in general. @JacobSzwejbka@angelayi what are your thoughts on this?

@GregoryComerGregoryComer changed the title Fix shape_env handling in SpecPropPassFix double-tracing in SpecPropPassNov 7, 2025
Comment threadexir/passes/spec_prop_pass.py Outdated
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 8, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 10, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 11, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@meta-codesync

Copy link
Copy Markdown
Contributor

@GregoryComer has imported this pull request. If you are a Meta employee, you can view this in D85913581.

@GregoryComer
GregoryComer marked this pull request as ready for review November 11, 2025 05:10
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Nov 12, 2025
Summary: Pull Request resolved: pytorch#15485
Differential Revision: D85913581
@GregoryComer

Copy link
Copy Markdown
ContributorAuthor

Note that the moshi and zephyr size test failures are pre-existing.

CopilotAI 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.

Pull request overview

This PR fixes a double-tracing issue in SpecPropPass where tensor specs were generated with symints from a different trace than the one used for guards, causing guards on unbacked symints to be lost. The fix refactors SpecPropPass to perform a single re-trace using the parent ExportPass class and then generate specs from the resulting metadata, ensuring consistency between specs and guards.

Key changes:

  • Rewrote SpecPropPass.__call__() to re-trace once and populate specs from meta values
  • Removed individual node handler methods (placeholder, call_operator, call_getitem, etc.) in favor of unified spec generation
  • Added test case with custom op using unbacked symints to verify guard propagation

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
exir/passes/spec_prop_pass.pyComplete rewrite of SpecPropPass to use single re-trace strategy; replaces per-node callbacks with post-trace spec generation from meta values
exir/tests/test_passes.pyAdds custom ops (unbacked, unbacked.out) and test case to verify spec propagation correctly captures guards for unbacked symints

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/passes/spec_prop_pass.py
Comment threadexir/tests/test_passes.py Outdated
Comment on lines +83 to +86
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

Consider merging these 2 conditions?

@GregoryComerGregoryComerDec 4, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

There is some weird existing behavior here that seems to need to be preserved (barring a larger update). Basically, we don't want to regenerate call_delegate node specs but do want to regenerate everything else. I'll add a comment detailing why.

Comment on lines -107 to -125
def call_cond(self, pred, true_fn, false_fn, inputs, meta):
# true_fn/false_fn return tensors of the same shape, so we can pick
# either one here.
*_, true_out_node = true_fn.graph.nodes
meta["spec"] = pytree.tree_map(make_spec, true_out_node.meta["val"])
return super().call_cond(pred, true_fn, false_fn, inputs, meta)

def call_while(
self,
cond_fn: torch.fx.GraphModule,
body_fn: torch.fx.GraphModule,
carried_inputs: List[ProxyValue],
additional_inputs: List[ProxyValue],
meta: NodeMetadata,
):
meta["spec"] = pytree.tree_map(make_spec, carried_inputs)
return super().call_while(
cond_fn, body_fn, carried_inputs, additional_inputs, meta
)

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.

Why we don't have to handle condition and while anymore?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

They should be handled by having the tracing logic use ExportPass to regenerate the meta values and then assigning spec values for each node correspondingly. I did go ahead and specific tests for cond and while to verify that the specs are generated correctly. As long as the cond + while outputs don't alias anything else (my understanding is that this should be the case), it should be good.

GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
GregoryComer added a commit to GregoryComer/executorch that referenced this pull request Dec 4, 2025
Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
Comment on lines +79 to +94
elif (
node.op == "call_function"
and node.target == executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if "spec" not in node.meta:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] = pytree.tree_map(make_spec, meta_val)

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.

I meant something like:

Suggested change
elif (
node.op=="call_function"
andnode.target==executorch_call_delegate
):
# Note: We currently rely on delegate node specs not being regenerated,
# as the spec is set somewhat manually when adding the call delegate node.
# If we regenerate, it can change and break lowering (it becomes a tuple?).
# Ideally, we should figure out how to make the spec regeneration not break
# things.
#
# We do need to regenerate non-call-delegate node specs, as this pass is called
# multiple times in some lowering paths (backends can and do call it).
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)
else:
if"spec"notinnode.meta:
node.meta["spec"] =pytree.tree_map(make_spec, meta_val)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure - the issue that I've seen is that sometimes this pass gets called multiple times (Cadence backend does this, for example) and thus we need to regenerate the spec for most nodes to make sure they pick up on any shape changes between calls.

But if we regenerate the spec for call_delegate nodes, it breaks things. So the if "spec" not in node.meta: condition should only apply to call_delegate but not anything else. Otherwise is breaks existing backend assumptions.

Ideally, we'll do a deeper change to fix this but this preserves the existing behavior. I could change the line to if "spec" not in node.meta or node.target != executorch_call_delegate if you'd prefer.

Summary:
Our current SpecPropPass doesn't properly capture the effect of guards in the shape environment due to double-tracing certain ops. The problem looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the spec and then once by calling super().call_operator(...) ([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and then generate specs based on the meta values, not the traced ProxyValues (thanks angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid adding a new dep to the core EXIR tests, I've written a test with a custom op that uses an unbacked symint in the meta kernel output shape to replicate the bug in the same way.
Differential Revision: D85913581
Pulled By: GregoryComer
@GregoryComer
GregoryComer merged commit 3ad2610 into pytorch:mainDec 8, 2025
299 of 302 checks passed
jirioc pushed a commit to nxp-upstream/executorch that referenced this pull request Dec 19, 2025
Our current SpecPropPass doesn't properly capture the effect of guards
in the shape environment due to double-tracing certain ops. The problem
looks like this:
* Every time we trace through the graph, we generate new symints.
* That's fine, since shape_env will pick up guards during the retrace.
* Problem is that SpecPropPass does this twice. Once to generate the
spec and then once by calling super().call_operator(...)
([https://github.com/.../exir/passes/spec_prop_pass.py...](https://github.com/pytorch/executorch/blob/11f752cf84b296a39c0b74b889d618f279bc8186/exir/passes/spec_prop_pass.py#L98)).
* The tensor spec gets the symint from the first. But the graph and
guards use the second.
* Hence the tensor spec doesn't pick up on guards.
To resolve this, I've updated the SpecPropPass to re-trace the graph and
then generate specs based on the meta values, not the traced ProxyValues
(thanks @angelayi for the suggestion). This resolves the issue.
I originally saw this issue with the NMS torchvision op, but to avoid
adding a new dep to the core EXIR tests, I've written a test with a
custom op that uses an unbacked symint in the meta kernel output shape
to replicate the bug in the same way.
Differential Revision: D85913581
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunkCLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.fb-exportedmeta-exportedrelease notes: exirChanges to any dialects and passes on these dialects, such as memory planning

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@GregoryComer@larryliu0820@angelayi