From 93c66b4d6f3b8b32c6c9a514a57fa23309637b3d Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Sat, 8 Aug 2026 19:14:30 -0700 Subject: [PATCH] Fix clamped_relu's value_inference for a negative beta clamped_relu is documented as "If x >= 0 return elementwise min(beta, x), otherwise return min(beta, alpha * x)". value_inference computes it by splitting x into a positive and a negative half, clamping each, and adding them: x = np.minimum(np.maximum(self.x.val, 0), self.beta.val) y = np.minimum(np.minimum(self.x.val, 0) * self.alpha.val, self.beta.val) return x + y That relies on exactly one of the two terms being 0, which stops holding once beta is negative: then both terms clamp to beta and the sum is 2 * beta. mb.clamped_relu(x=np.array([-2., 0., 3.], dtype=np.float32), alpha=0.5, beta=-1.0) got [-2. -2. -2.] correct [-1. -1. -1.] Negative beta is a supported configuration: the op lowers to the neural network add_clamped_relu layer, documented as f(x) = min((x >= 0 ? x : alpha * x), beta), whose backend test already parametrizes beta over [7.0, -8.0] (test_numpy_nn_layers.py::test_clamped_relu_cpu). Apply the documented formula directly. Verified against that same backend expectation for every alpha in [0.0, 2.0, -3.0] and beta in [7.0, -8.0]. --- .../mil/mil/ops/defs/iOS15/activation.py | 11 ++++++++--- .../mil/mil/ops/tests/iOS14/test_activation.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/coremltools/converters/mil/mil/ops/defs/iOS15/activation.py b/coremltools/converters/mil/mil/ops/defs/iOS15/activation.py index 0c261a1f2..aede13d7d 100644 --- a/coremltools/converters/mil/mil/ops/defs/iOS15/activation.py +++ b/coremltools/converters/mil/mil/ops/defs/iOS15/activation.py @@ -77,9 +77,14 @@ class clamped_relu(activation_with_alpha_and_beta): @precondition(allow=VALUE) def value_inference(self): - x = np.minimum(np.maximum(self.x.val, 0), self.beta.val) - y = np.minimum(np.minimum(self.x.val, 0) * self.alpha.val, self.beta.val) - return x + y + # Splitting into a positive and a negative half and adding them back together + # only works when at most one half is non-zero, which stops being true once + # beta is negative and clamps both of them. Apply the documented formula + # directly instead. + return np.minimum( + np.where(self.x.val >= 0, self.x.val, self.x.val * self.alpha.val), + self.beta.val, + ) @register_op diff --git a/coremltools/converters/mil/mil/ops/tests/iOS14/test_activation.py b/coremltools/converters/mil/mil/ops/tests/iOS14/test_activation.py index c80f90ddb..e851853df 100644 --- a/coremltools/converters/mil/mil/ops/tests/iOS14/test_activation.py +++ b/coremltools/converters/mil/mil/ops/tests/iOS14/test_activation.py @@ -55,6 +55,20 @@ def test_builder_eval(self): y = np.minimum(np.minimum(x_val, 0) * 2.0, 1.0) np.testing.assert_allclose(x + y, v.val, atol=1e-04, rtol=1e-05) + @ssa_fn + def test_builder_eval_negative_beta(self): + """ + Constant folding must agree with the runtime contract + ``f(x) = min((x >= 0 ? x : alpha * x), beta)`` for a negative beta too. + The alpha / beta combinations here match the ones the neural network backend + test for this layer already covers (test_numpy_nn_layers.test_clamped_relu_cpu). + """ + x_val = np.arange(-20, 20, dtype=np.float32) + for alpha, beta in itertools.product([0.0, 2.0, -3.0], [7.0, -8.0]): + v = mb.clamped_relu(x=x_val, alpha=alpha, beta=beta) + expected = np.minimum(beta, np.where(x_val >= 0, x_val, x_val * alpha)) + np.testing.assert_allclose(expected, v.val, atol=1e-04, rtol=1e-05) + @pytest.mark.parametrize( "compute_unit, backend, dim, alpha, beta", itertools.product(compute_units, backends, [2, 4, 8], [2.0, 3.0], [4.0, 5.0]),