Skip to content

merge_consecutive_reshapes: do not merge past a trailing shape containing 0 - #2790

Open
LeSingh1 wants to merge 1 commit into
apple:mainfrom
LeSingh1:merge-reshapes-zero-dim
Open

merge_consecutive_reshapes: do not merge past a trailing shape containing 0#2790
LeSingh1 wants to merge 1 commit into
apple:mainfrom
LeSingh1:merge-reshapes-zero-dim

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Problem

Per the reshape op's own docstring, a 0 in shape means "if K == rank(x) then 0 means inheriting from the corresponding dimension in x.shape".

merge_consecutive_reshapes builds the merged op as mb.reshape(x=reshape_ops[0].x, shape=reshape_ops[-1].shape) — shape from the last op of the sequence, input from the first. So a 0 in the trailing shape is re-bound to a different tensor and the model's output shape silently changes:

@mb.program(input_specs=[mb.TensorSpec(shape=(2, 3, 4))])
def prog(x):
    y = mb.reshape(x=x, shape=(4, 3, 2))
    return mb.reshape(x=y, shape=(0, 6, -1))

apply_pass_and_basic_check(prog, "common::merge_consecutive_reshapes")
# ['reshape', 'reshape'] -> ['reshape']
# output shape (4, 6, 1) -> (2, 6, 2)

The 0 originally inherited dim 0 of (4, 3, 2) = 4; after the merge it inherits dim 0 of (2, 3, 4) = 2. The -1 then absorbs the difference, so the volume still checks out and nothing raises — the converted model just has a different output shape than the program it was built from.

common::merge_consecutive_reshapes is in the default pipeline (pass_pipeline.py).

Fix

Trim ops whose shape contains a 0 off the end of the matched sequence, rather than rejecting the sequence outright. Only the trailing position matters — a 0 anywhere earlier has its shape discarded by the merge and stays harmless — so the reshapes before the offending one still merge and the optimization is preserved as far as it is valid.

When the shape is not known at compile time (shape.sym_val is None) we cannot tell whether it holds a 0; that case keeps the existing behaviour rather than disabling the merge for all dynamically-shaped reshapes.

Tests

In TestMergeConsecutiveReshapes:

  • test_keep_trailing_reshape_inheriting_input_dims — the repro above; both reshapes must survive and the output shape must stay (4, 6, 1). Fails on main.
  • test_merge_up_to_reshape_inheriting_input_dims — three reshapes with a 0 in the last; the first two must still merge, leaving two ops. Fails on main.
  • test_merge_reshape_with_leading_zero_shape — a 0 in the first reshape must not block the merge (guards against over-narrowing). Passes before and after.

The existing tests in the class are untouched. I ran the whole class before and after; the failure sets are identical (this environment cannot load CoreML.framework, so the assert_model_is_valid predictions fail there either way — the graph-structure assertions preceding them all run and pass).

Note: this touches optimize_repeat_ops.py, which my PR #2784 also modifies, in a different class.

…ning 0

A 0 in reshape's shape means "inherit the corresponding dimension from x.shape"
(see the reshape op docstring). The merged reshape takes its shape from the last
op of the sequence but its input from the first, so a trailing 0 gets re-bound to
a different tensor and the model's output shape silently changes:

    @mb.program(input_specs=[mb.TensorSpec(shape=(2, 3, 4))])
    def prog(x):
        y = mb.reshape(x=x, shape=(4, 3, 2))
        return mb.reshape(x=y, shape=(0, 6, -1))

    ['reshape', 'reshape'] -> ['reshape'],  output shape (4, 6, 1) -> (2, 6, 2)

The 0 originally inherited dim 0 of (4, 3, 2); after the merge it inherits dim 0
of (2, 3, 4).

Trim such ops off the end of the matched sequence rather than rejecting the whole
sequence, so the reshapes before the offending one still merge. A 0 anywhere but
the last position is discarded by the merge and remains harmless.

When the shape is not known at compile time we cannot tell whether it holds a 0;
that case keeps the existing behavior.

common::merge_consecutive_reshapes is in the default pipeline.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant