Skip to content

fix(optimizer): move subgraph initializers to main graph when inlining If branches#2887

Open
jaytiwarihub wants to merge 1 commit intomicrosoft:mainfrom
jaytiwarihub:fix-optimizer
Open

fix(optimizer): move subgraph initializers to main graph when inlining If branches#2887
jaytiwarihub wants to merge 1 commit intomicrosoft:mainfrom
jaytiwarihub:fix-optimizer

Conversation

@jaytiwarihub
Copy link
Copy Markdown

Fixes #2810

When FoldConstantsPass inlines an If branch (because its condition is a known constant), initializers belonging to the branch subgraph were silently abandoned. This caused downstream nodes that referenced those initializers — most visibly Squeeze nodes whose axes tensor had been folded in a prior pass — to have dangling inputs, producing an invalid ONNX model that fails schema validation.

Cause

The if_op partial evaluator correctly moves the branch's nodes into the main graph via Replacement, but it never touched the branch's initializers. Initializers are stored on the ir.Graph object itself, not on individual nodes, so they were invisible to the node-migration logic. After inlining, those values existed nowhere in the main graph.

Fix

Added a helper _move_initializers_to_graph(src, dst) that migrates all initializers from a subgraph to the destination graph before the Replacement is returned, with name-collision handling.

Testing

Added two new tests to _constant_folding_test.py:

  • test_fold_if_cond_with_subgraph_initializer: Reproduces the exact two-pass scenario from the bug report. The first pass folds a constant inside the branch into a subgraph initializer; the second pass inlines the branch and verifies the initializer is successfully migrated to the main graph and that the model passes onnx.checker.check_model.
  • test_fold_if_cond_with_subgraph_initializer_name_collision: Verifies that name collisions between subgraph and main-graph initializers are handled gracefully via unique suffixing, preventing data loss or crashes during the migration.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Fixes a correctness issue in constant folding where inlined If branches could leave behind subgraph initializers, producing invalid ONNX graphs with dangling value references.

Changes:

  • Add _move_initializers_to_graph(src, dst) and call it from if_op before returning the inlining Replacement
  • Add regression tests covering subgraph-initializer migration and a “name collision” scenario
  • Minor formatting cleanups in tests and comments/log messages

Reviewed changes

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

File Description
onnxscript/optimizer/_constant_folding.py Migrates initializers from an inlined If branch subgraph into the main graph and does some minor doc/log cleanup.
onnxscript/optimizer/_constant_folding_test.py Adds regression tests for the initializer-migration bug and adjusts formatting of several existing test definitions/comments.
Comments suppressed due to low confidence (1)

onnxscript/optimizer/_constant_folding.py:1457

  • There’s trailing whitespace on the return line. Also, removing # type: ignore[return-value] may reintroduce static type-check failures if InPlacePass.__call__ isn’t typed to return FoldConstantsResult. If the repo runs mypy/pyright, consider either restoring the targeted ignore or adjusting the pass/call typing so folder_pass(model) is correctly typed without ignores.
def fold_constants(

return op.Constant(value_int=size)


def _move_initializers_to_graph(src: ir.Graph, dst: ir.Graph) -> None:
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

This helper guarantees uniqueness only against dst.initializers, but ONNX value names must be unique across the whole graph namespace (e.g., node outputs). If name collides with an existing non-initializer value name in dst, register_initializer may raise or the graph may become invalid. Also, popping from src.initializers before successfully registering can irreversibly drop the initializer if an exception occurs. Consider: (1) building a used_names set for the destination graph covering all existing value names (not just initializers), (2) choosing new_name against that set, and (3) only removing from src.initializers after the destination registration succeeds.

Copilot uses AI. Check for mistakes.
Comment on lines +566 to +575
for name in list(src.initializers):
initializer = src.initializers.pop(name)
# Ensure name uniqueness in the destination graph
new_name = name
while new_name in dst.initializers:
counter[name] = counter.get(name, 0) + 1
new_name = f"{name}_{counter[name]}"
if new_name != name:
initializer.name = new_name
dst.register_initializer(initializer)
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

This helper guarantees uniqueness only against dst.initializers, but ONNX value names must be unique across the whole graph namespace (e.g., node outputs). If name collides with an existing non-initializer value name in dst, register_initializer may raise or the graph may become invalid. Also, popping from src.initializers before successfully registering can irreversibly drop the initializer if an exception occurs. Consider: (1) building a used_names set for the destination graph covering all existing value names (not just initializers), (2) choosing new_name against that set, and (3) only removing from src.initializers after the destination registration succeeds.

Copilot uses AI. Check for mistakes.
if main_graph is not None:
_move_initializers_to_graph(graph, main_graph)

# TODO: we should handle initializers as well!
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The TODO on line 623 is now stale/contradictory because the function is explicitly handling initializers. Please remove or update that TODO to avoid misleading future readers.

Suggested change
# TODO: we should handle initializers as well!

Copilot uses AI. Check for mistakes.
should_fold: An optional function that takes a node and returns True if
the node should be considered for folding.
The function should return True/False value to indicate if this particular
The function should return True/False value to indicate if this particular
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

In the class docstring, the sentence on line 994 is mis-indented relative to the should_fold: attribute description, which makes the rendered doc confusing (it reads like a new attribute). Re-indent it so it’s clearly part of should_fold’s description.

Suggested change
The function should return True/False value to indicate if this particular
The function should return True/False value to indicate if this particular

Copilot uses AI. Check for mistakes.
should_fold=should_fold,
)
return folder_pass(model) # type: ignore[return-value]
return folder_pass(model) No newline at end of file
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

There’s trailing whitespace on the return line. Also, removing # type: ignore[return-value] may reintroduce static type-check failures if InPlacePass.__call__ isn’t typed to return FoldConstantsResult. If the repo runs mypy/pyright, consider either restoring the targeted ignore or adjusting the pass/call typing so folder_pass(model) is correctly typed without ignores.

Suggested change
return folder_pass(model)
return typing.cast(FoldConstantsResult, folder_pass(model))

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +57
def test_fold_if_cond_with_subgraph_initializer_name_collision(self):
"""Subgraph initializer names that clash with main-graph names get a unique suffix."""
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

This test currently asserts only that it 'must not crash' and that the final model passes onnx.checker.check_model, but it does not verify that a name collision actually occurred or that suffixing happened (and the chosen graph text may not force a collision depending on what gets folded into initializers). To make this a robust regression test for the collision logic, assert that: (1) the main graph already contains an initializer with the colliding name before inlining, (2) after inlining there are two distinct initializers (original + suffixed), and (3) the inlined nodes reference the suffixed initializer (not the original).

Copilot uses AI. Check for mistakes.
def test_static_split_to_sequence_with_scalar_split_and_squence_at_is_folded_as_split(
self,
):
def test_static_split_to_sequence_with_scalar_split_and_squence_at_is_folded_as_split(self):
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The word 'squence' appears to be a misspelling of 'sequence' in these updated test names. Since these lines were modified in this PR, it’s a good opportunity to correct the spelling for clarity and searchability.

Suggested change
def test_static_split_to_sequence_with_scalar_split_and_squence_at_is_folded_as_split(self):
def test_static_split_to_sequence_with_scalar_split_and_sequence_at_is_folded_as_split(self):

Copilot uses AI. Check for mistakes.
def test_static_split_to_sequence_with_list_split_and_squence_at_is_folded_as_split(
self,
):
def test_static_split_to_sequence_with_list_split_and_squence_at_is_folded_as_split(self):
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The word 'squence' appears to be a misspelling of 'sequence' in these updated test names. Since these lines were modified in this PR, it’s a good opportunity to correct the spelling for clarity and searchability.

Copilot uses AI. Check for mistakes.
def test_static_split_to_sequence_with_list_split_no_keepdims_and_squence_at_is_folded_as_split_with_squeeze(
self,
):
def test_static_split_to_sequence_with_list_split_no_keepdims_and_squence_at_is_folded_as_split_with_squeeze(self):
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The word 'squence' appears to be a misspelling of 'sequence' in these updated test names. Since these lines were modified in this PR, it’s a good opportunity to correct the spelling for clarity and searchability.

Copilot uses AI. Check for mistakes.
@jaytiwarihub
Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

should_fold=should_fold,
)
return folder_pass(model) # type: ignore[return-value]
return folder_pass(model) No newline at end of file
should_fold=should_fold,
)
return folder_pass(model) # type: ignore[return-value]
return folder_pass(model) No newline at end of file
should_fold=should_fold,
)
return folder_pass(model) # type: ignore[return-value]
return folder_pass(model) No newline at end of file

if __name__ == "__main__":
unittest.main()
unittest.main() No newline at end of file
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 85.10638% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.26%. Comparing base (12234f8) to head (c75a37d).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
onnxscript/optimizer/_constant_folding.py 60.00% 3 Missing and 3 partials ⚠️
onnxscript/optimizer/_constant_folding_test.py 96.87% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2887      +/-   ##
==========================================
+ Coverage   72.02%   72.26%   +0.23%     
==========================================
  Files         239      241       +2     
  Lines       29309    29690     +381     
  Branches     2882     2918      +36     
==========================================
+ Hits        21110    21455     +345     
- Misses       7219     7238      +19     
- Partials      980      997      +17     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

Constant Folding removes 'axes' initializer form Squeeze layer, resulting in incorrect ONNX model

3 participants