Skip to content

Copy the input in thresholded_relu's value_inference - #2795

Open
LeSingh1 wants to merge 1 commit into
apple:mainfrom
LeSingh1:thresholded-relu-inplace
Open

Copy the input in thresholded_relu's value_inference#2795
LeSingh1 wants to merge 1 commit into
apple:mainfrom
LeSingh1:thresholded-relu-inplace

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Problem

thresholded_relu.value_inference aliases its input's array and then writes through it:

@precondition(allow=VALUE)
def value_inference(self):
    y = self.x.val          # alias, not a copy
    y[y < self.alpha.val] = 0
    return y

Every sibling op in the same file copies first — elu (line 107), leaky_relu (line 210), softplus_parametric (lines 497-498) all start with np.copy(...). thresholded_relu does not, so constant-folding it overwrites the value of its input const.

When that const is also used elsewhere — as a weight, say — the weight is silently replaced with the thresholded values:

w_val = np.array([[-1.0, 0.5], [2.0, -3.0]], dtype=np.float32)

@mb.program(input_specs=[mb.TensorSpec(shape=(2, 2))])
def prog(x):
    w = mb.const(val=w_val, name="weight")
    y = mb.matmul(x=x, y=w)
    z = mb.thresholded_relu(x=w, alpha=1.0)
    return mb.add(x=y, y=z)
value
matmul's weight operand [[0.0, 0.0], [2.0, 0.0]]
correct [[-1.0, 0.5], [2.0, -3.0]]

The corrupted array is what gets serialized into the weight blob. The caller's own numpy array is clobbered as well, since the const does not copy it on the way in.

Fix

y = np.copy(self.x.val), matching the other activation ops in the file.

Tests

In TestThresholdedReLU (ops/tests/iOS14/test_activation.py):

  • test_builder_eval_does_not_mutate_input — the input array must be unchanged after folding, and the folded value must still be correct.
  • test_builder_eval_does_not_corrupt_shared_const — the shared-weight program above; matmul's operand must keep its original value.

Both fail on main and pass with the fix.

The existing test_builder_eval is untouched and still passes. It happens to compute its expected value as y = x_val; y[y < 2.0] = 0 on the same array value_inference was mutating, which is why it never caught this.

I ran all of ops/tests/iOS14/test_activation.py before and after; the pre-existing failure set is identical, 188 either way (this environment cannot load CoreML.framework, so the run_compare_builder tests fail there regardless).

value_inference aliases the input's array and then writes through it:

    y = self.x.val
    y[y < self.alpha.val] = 0
    return y

so folding a thresholded_relu overwrites the value of its input const. Every
sibling op in the file (elu, leaky_relu, softplus_parametric) copies first.

When the const is also used as a weight elsewhere, that weight is silently
replaced with the thresholded values:

    @mb.program(input_specs=[mb.TensorSpec(shape=(2, 2))])
    def prog(x):
        w = mb.const(val=np.array([[-1., .5], [2., -3.]], dtype=np.float32))
        y = mb.matmul(x=x, y=w)
        z = mb.thresholded_relu(x=w, alpha=1.0)
        return mb.add(x=y, y=z)

    matmul's weight becomes [[0., 0.], [2., 0.]]

The caller's own numpy array is clobbered too, since the const does not copy it.

Use np.copy, matching the other activation ops.
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