Skip to content

fuse_reduce_mean: do not fuse when the scalar operand raises the rank - #2792

Open
LeSingh1 wants to merge 1 commit into
apple:mainfrom
LeSingh1:reduce-mean-rank
Open

fuse_reduce_mean: do not fuse when the scalar operand raises the rank#2792
LeSingh1 wants to merge 1 commit into
apple:mainfrom
LeSingh1:reduce-mean-rank

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Problem

fuse_reduce_mean's docstring says the multiplier is a const (scalar). The code validates it with _check_var_scalar_value, which accepts any tensor whose size == 1, regardless of rank:

if isinstance(x.val, np.ndarray):
    if x.val.size != 1:
        return False

A shape (1, 1, 1) const holding 1/count therefore passes the check. But mul broadcasts reduce_sum's output up to rank 3, and the reduce_mean that replaces the pair does not:

@mb.program(input_specs=[mb.TensorSpec(shape=(2, 3))])
def prog(x):
    s = mb.reduce_sum(x=x, axes=[1], keep_dims=False)          # (2,)
    return mb.mul(x=s, y=np.array([[[1.0 / 3]]], dtype=np.float32))

PASS_REGISTRY["common::fuse_reduce_mean"](prog)
# ['reduce_sum', 'mul'] -> ['reduce_mean']
# output shape (1, 1, 2) -> (2,)

The real_div branch has the same problem with a shape (1, 1, 1) count. Both are the model's declared output, so the converted model has a different output shape than the program it was built from.

common::fuse_reduce_mean is in the default pipeline.

Fix

Check that the child op's output shape matches reduce_sum's output shape before rewriting. A size-1 operand that does not raise the rank (a plain scalar, or a shape (1,) const against a rank-1 reduce output) broadcasts to the same shape and keeps fusing, so the pattern the pass actually targets is unaffected.

I deliberately put the check in this pass rather than tightening _check_var_scalar_value, which is shared with several other passes that may legitimately want the looser size-1 test.

Tests

In TestReduceMeanFusion:

  • test_invalid_pattern_rank_raising_mul — the repro above. Fails on main.
  • test_invalid_pattern_rank_raising_real_div — same through the real_div branch. Fails on main.
  • test_valid_pattern_size_one_tensor_multiplier — a shape (1,) multiplier that does not raise the rank must still fuse (guards against over-narrowing). Passes before and after.

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

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

fuse_reduce_mean validates the mul / real_div operand with
_check_var_scalar_value, which accepts any tensor whose size is 1 regardless of
its rank. A shape (1, 1, 1) const holding 1/count therefore passes, but the
mul broadcasts reduce_sum's output up to rank 3, and the reduce_mean that
replaces the pair does not:

    @mb.program(input_specs=[mb.TensorSpec(shape=(2, 3))])
    def prog(x):
        s = mb.reduce_sum(x=x, axes=[1], keep_dims=False)   # (2,)
        return mb.mul(x=s, y=np.array([[[1.0 / 3]]], dtype=np.float32))

    ['reduce_sum', 'mul'] -> ['reduce_mean'],  output shape (1, 1, 2) -> (2,)

The real_div branch has the same problem with a shape (1, 1, 1) count.

Check that the child op's output shape matches reduce_sum's output shape before
rewriting. A size-1 operand that does not raise the rank broadcasts to the same
shape and keeps fusing, so the pattern the pass targets is unaffected.

The check is local to this pass rather than in _check_var_scalar_value, which is
shared with several other passes.
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