From 80e38da03d16c7dc7d61dea1c0eb329a837b6c6f Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Tue, 10 Jun 2025 18:40:07 +0800 Subject: [PATCH 01/21] WIP: median support for OpenVINO back-end --- .../openvino/excluded_concrete_tests.txt | 2 - keras/src/backend/openvino/numpy.py | 69 ++++++++++++++++++- pytest.ini | 3 + 3 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 pytest.ini diff --git a/keras/src/backend/openvino/excluded_concrete_tests.txt b/keras/src/backend/openvino/excluded_concrete_tests.txt index 48dd1ffb49e9..bbb0ae427bf9 100644 --- a/keras/src/backend/openvino/excluded_concrete_tests.txt +++ b/keras/src/backend/openvino/excluded_concrete_tests.txt @@ -38,7 +38,6 @@ NumpyDtypeTest::test_logspace NumpyDtypeTest::test_matmul_ NumpyDtypeTest::test_max NumpyDtypeTest::test_mean -NumpyDtypeTest::test_median NumpyDtypeTest::test_meshgrid NumpyDtypeTest::test_minimum_python_types NumpyDtypeTest::test_multiply @@ -95,7 +94,6 @@ NumpyOneInputOpsCorrectnessTest::test_isinf NumpyOneInputOpsCorrectnessTest::test_logaddexp NumpyOneInputOpsCorrectnessTest::test_max NumpyOneInputOpsCorrectnessTest::test_mean -NumpyOneInputOpsCorrectnessTest::test_median NumpyOneInputOpsCorrectnessTest::test_meshgrid NumpyOneInputOpsCorrectnessTest::test_pad_float16_constant_2 NumpyOneInputOpsCorrectnessTest::test_pad_float32_constant_2 diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index 4f9fae1c986f..dad062f11c22 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1046,8 +1046,73 @@ def maximum(x1, x2): def median(x, axis=None, keepdims=False): - raise NotImplementedError("`median` is not supported with openvino backend") - + x = get_ov_output(x) + x_shape_original = ov_opset.shape_of(x).output(0) + + if axis is None: + flatten_shape = ov_opset.constant([-1], Type.i32).output(0) + x = ov_opset.reshape(x, flatten_shape, False).output(0) + axis = 0 + flattened = True + int_axis = False + x_shape = ov_opset.shape_of(x).output(0) + k_value = ov_opset.convert(x_shape, Type.i32).output(0) + elif isinstance(axis, int): + flattened = False + int_axis = True + ov_axis = ov_opset.constant(axis, Type.i32).output(0) + x_shape = ov_opset.shape_of(x).output(0) + k_value = ov_opset.convert(ov_opset.gather(x_shape, ov_axis, ov_opset.constant([0], Type.i32).output(0)).output(0), Type.i32).output(0) + else: + # axis = (2, 1) + flattened = False + int_axis = False + ov_axis = ov_opset.constant(axis, Type.i32).output(0) # (2, 1) + x_rank = ov_opset.shape_of(x_shape_original).output(0) # 4 + axis_range = ov_opset.range(ov_opset.constant([0], Type.i32).output(0), x_rank, ov_opset.constant([1], Type.i32).output(0)).output(0) + axis_compare = ov_opset.equal(ov_opset.unsqueeze(ov_axis, 1).output(0), ov_opset.unsqueeze(axis_range, 0).output(0)).output(0) + mask_remove = ov_opset.reduce_logical_or(axis_compare, ov_opset.constant([0], Type.i32).output(0)).output(0) + mask_keep = ov_opset.logical_not(mask_remove).output(0) + nz = ov_opset.non_zero(mask_keep, "i32").output(0) + indices_keep = ov_opset.squeeze(nz, [0]).output(0) + axis_range = ov_opset.gather(axis_range, indices_keep, ov_opset.constant([0], Type.i32).output(0)).output(0) # (0, 3) + axis_range = ov_opset.concat([axis_range, ov_axis], ov_opset.constant([0], Type.i32).output(0)).output(0) # (0, 3, 2, 1) + x = ov_opset.transpose(x, axis_range).output(0) # x = (d0, d3, d2, d1) + + flat_rank = ov_opset.subtract(x_rank, ov_opset.constant([1], Type.i32)).output(0) + flatten_shape = ov_opset.constant([0], shape=flat_rank, type_info=Type.i32).output(0) + flatten_shape = ov_opset.scatter_elements_update(flatten_shape, ov_opset.constant([-1], Type.i32).output(0), [-1], [0], "sum") + + x = ov_opset.reshape(x, flatten_shape, True).output(0) # x = (d0, d3, d2*d1) + axis = -1 + x_shape = ov_opset.shape_of(x).output(0) + k_value = ov_opset.gather(x_shape, ov_opset.constant([-1], Type.i32).output(0), ov_opset.constant([0], Type.i32).output(0)).output(0) + k_value = ov_opset.convert(k_value, Type.i32).output(0) + + x_sorted = ov_opset.topk(x, k_value, axis, 'min', 'value', stable=True).output(0) + half_index = ov_opset.divide(k_value, ov_opset.constant([2], Type.i32)).output(0) + x_mod = ov_opset.mod(k_value, ov_opset.constant([2], Type.i32)).output(0) + is_even = ov_opset.equal(x_mod, ov_opset.constant([0], Type.i32)).output(0) + med_index_0 = ov_opset.gather(x_sorted, ov_opset.floor(half_index).output(0), axis).output(0) # COME BACK, does it sort out higher dimensions? + med_index_1 = ov_opset.gather(x_sorted, ov_opset.add(med_index_0, ov_opset.constant([1], Type.i32)).output(0), axis).output(0) + + median_odd = med_index_0 + median_even = ov_opset.divide(ov_opset.add(med_index_1, med_index_0).output(0), ov_opset.constant([2], Type.i32)) + + median_eval = ov_opset.select(is_even, median_even, median_odd) + + if keepdims == True: + if flattened == True: + median_shape = ov_opset.divide(x_shape_original, x_shape_original).output(0) + median_eval = ov_opset.reshape(median_eval, median_shape, False).output(0) + elif int_axis == True: + median_shape = ov_opset.shape_of(median_eval).output(0) + median_shape = ov_opset.unsqueeze(median_shape, axis).output(0) + median_eval = ov_opset.reshape(median_eval, median_shape, False).output(0) + else: + median_eval = ov_opset.unsqueeze(median_eval, ov_axis).output(0) + + return OpenVINOKerasTensor(median_eval) def meshgrid(*x, indexing="xy"): raise NotImplementedError( diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000000..83635a5b7b9b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +env = + KERAS_BACKEND=openvino \ No newline at end of file From 98f982eb9675e11ebf6a0e76c33b8568d2abbf85 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 11:41:09 +0800 Subject: [PATCH 02/21] Finished median(), linted --- keras/src/backend/openvino/numpy.py | 174 ++++++++++++++++++++-------- 1 file changed, 124 insertions(+), 50 deletions(-) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index dad062f11c22..01df0cccb43c 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1046,74 +1046,148 @@ def maximum(x1, x2): def median(x, axis=None, keepdims=False): + if np.isscalar(x): + x = get_ov_output(x) + return OpenVINOKerasTensor(x) + x = get_ov_output(x) - x_shape_original = ov_opset.shape_of(x).output(0) - + x_type = x.get_element_type() + if x_type == Type.boolean or x_type.is_integral(): + x = ov_opset.convert(x, Type.f32).output(0) + x_type = x.get_element_type() + x_shape_original = ov_opset.shape_of(x, Type.i32).output(0) + if axis is None: flatten_shape = ov_opset.constant([-1], Type.i32).output(0) x = ov_opset.reshape(x, flatten_shape, False).output(0) axis = 0 + ov_axis = get_ov_output(axis) flattened = True - int_axis = False - x_shape = ov_opset.shape_of(x).output(0) - k_value = ov_opset.convert(x_shape, Type.i32).output(0) + k_value = ov_opset.gather( + ov_opset.shape_of(x, Type.i32).output(0), + ov_opset.constant([0], Type.i32).output(0), + ov_axis, + ).output(0) elif isinstance(axis, int): flattened = False - int_axis = True - ov_axis = ov_opset.constant(axis, Type.i32).output(0) - x_shape = ov_opset.shape_of(x).output(0) - k_value = ov_opset.convert(ov_opset.gather(x_shape, ov_axis, ov_opset.constant([0], Type.i32).output(0)).output(0), Type.i32).output(0) + ov_axis = get_ov_output(axis) + x_shape = ov_opset.shape_of(x, Type.i32).output(0) + k_value = ov_opset.gather( + x_shape, ov_axis, ov_opset.constant([0], Type.i32).output(0) + ).output(0) else: - # axis = (2, 1) flattened = False - int_axis = False - ov_axis = ov_opset.constant(axis, Type.i32).output(0) # (2, 1) - x_rank = ov_opset.shape_of(x_shape_original).output(0) # 4 - axis_range = ov_opset.range(ov_opset.constant([0], Type.i32).output(0), x_rank, ov_opset.constant([1], Type.i32).output(0)).output(0) - axis_compare = ov_opset.equal(ov_opset.unsqueeze(ov_axis, 1).output(0), ov_opset.unsqueeze(axis_range, 0).output(0)).output(0) - mask_remove = ov_opset.reduce_logical_or(axis_compare, ov_opset.constant([0], Type.i32).output(0)).output(0) - mask_keep = ov_opset.logical_not(mask_remove).output(0) - nz = ov_opset.non_zero(mask_keep, "i32").output(0) - indices_keep = ov_opset.squeeze(nz, [0]).output(0) - axis_range = ov_opset.gather(axis_range, indices_keep, ov_opset.constant([0], Type.i32).output(0)).output(0) # (0, 3) - axis_range = ov_opset.concat([axis_range, ov_axis], ov_opset.constant([0], Type.i32).output(0)).output(0) # (0, 3, 2, 1) - x = ov_opset.transpose(x, axis_range).output(0) # x = (d0, d3, d2, d1) - - flat_rank = ov_opset.subtract(x_rank, ov_opset.constant([1], Type.i32)).output(0) - flatten_shape = ov_opset.constant([0], shape=flat_rank, type_info=Type.i32).output(0) - flatten_shape = ov_opset.scatter_elements_update(flatten_shape, ov_opset.constant([-1], Type.i32).output(0), [-1], [0], "sum") - - x = ov_opset.reshape(x, flatten_shape, True).output(0) # x = (d0, d3, d2*d1) + ov_axis = get_ov_output(axis) + x_rank = ov_opset.gather( + ov_opset.shape_of(x_shape_original, Type.i32).output(0), + ov_opset.constant([0], Type.i32).output(0), + ).output(0) + axis_as_range = ov_opset.range( + ov_opset.constant([0], Type.i32).output(0), + x_rank, + ov_opset.constant([1], Type.i32).output(0), + ).output(0) + axis_compare = ov_opset.not_equal( + ov_opset.unsqueeze(axis_as_range, 1).output(0), + ov_opset.unsqueeze(ov_axis, 0).output(0), + "NUMPY", + ).output(0) + keep_axes = ov_opset.reduce_logical_or( + axis_compare, ov_opset.constant([1], Type.i32).output(0) + ).output(0) + nz = ov_opset.non_zero(keep_axes, Type.i32).output(0) + keep_axes = ov_opset.reduce_sum( + nz, ov_opset.constant([1], Type.i32).output(0) + ).output(0) + reordered_axes = ov_opset.concat( + [keep_axes, ov_axis], ov_opset.constant([0], Type.i32).output(0) + ).output(0) + x = ov_opset.transpose(x, reordered_axes).output(0) + + flat_rank = ov_opset.subtract( + x_rank, ov_opset.constant([1], Type.i32) + ).output(0) + flatten_shape = ov_opset.broadcast( + ov_opset.constant([0], Type.i32).output(0), flat_rank + ).output(0) + flatten_shape = ov_opset.scatter_elements_update( + flatten_shape, + ov_opset.constant([-1], Type.i32).output(0), + ov_opset.constant([-1], Type.i32).output(0), + 0, + "sum", + ).output(0) + + x = ov_opset.reshape(x, flatten_shape, True).output(0) axis = -1 - x_shape = ov_opset.shape_of(x).output(0) - k_value = ov_opset.gather(x_shape, ov_opset.constant([-1], Type.i32).output(0), ov_opset.constant([0], Type.i32).output(0)).output(0) - k_value = ov_opset.convert(k_value, Type.i32).output(0) - - x_sorted = ov_opset.topk(x, k_value, axis, 'min', 'value', stable=True).output(0) - half_index = ov_opset.divide(k_value, ov_opset.constant([2], Type.i32)).output(0) + x_shape = ov_opset.shape_of(x, Type.i32).output(0) + k_value = ov_opset.gather( + x_shape, + ov_opset.constant([-1], Type.i32).output(0), + ov_opset.constant([0], Type.i32).output(0), + ).output(0) + + if axis < 0: + x_rank = ov_opset.gather( + ov_opset.shape_of(x, Type.i32).output(0), + ov_opset.constant([0], Type.i32).output(0), + ).output(0) + axis_as_range = ov_opset.range( + ov_opset.constant([0], Type.i32).output(0), + x_rank, + ov_opset.constant([1], Type.i32).output(0), + ).output(0) + ov_axis_positive = ov_opset.gather( + axis_as_range, ov_axis, ov_opset.constant([0], Type.i32) + ).output(0) + else: + ov_axis_positive = ov_axis + + x_sorted = ov_opset.topk( + x, k_value, axis, "min", "value", stable=True + ).output(0) + half_index = ov_opset.floor( + ov_opset.divide(k_value, ov_opset.constant([2], Type.i32)).output(0) + ).output(0) + half_index = ov_opset.convert(half_index, Type.i32).output(0) x_mod = ov_opset.mod(k_value, ov_opset.constant([2], Type.i32)).output(0) is_even = ov_opset.equal(x_mod, ov_opset.constant([0], Type.i32)).output(0) - med_index_0 = ov_opset.gather(x_sorted, ov_opset.floor(half_index).output(0), axis).output(0) # COME BACK, does it sort out higher dimensions? - med_index_1 = ov_opset.gather(x_sorted, ov_opset.add(med_index_0, ov_opset.constant([1], Type.i32)).output(0), axis).output(0) - - median_odd = med_index_0 - median_even = ov_opset.divide(ov_opset.add(med_index_1, med_index_0).output(0), ov_opset.constant([2], Type.i32)) - + + med_0 = ov_opset.gather(x_sorted, half_index, ov_axis_positive).output(0) + med_1 = ov_opset.select( + is_even, + ov_opset.gather( + x_sorted, + ov_opset.subtract( + half_index, ov_opset.constant([1], Type.i32) + ).output(0), + ov_axis_positive, + ).output(0), + med_0, + ).output(0) + + median_odd = med_0 + median_even = ov_opset.divide( + ov_opset.add(med_1, med_0).output(0), + ov_opset.constant([2], Type.f32), + ) + median_eval = ov_opset.select(is_even, median_even, median_odd) - - if keepdims == True: - if flattened == True: - median_shape = ov_opset.divide(x_shape_original, x_shape_original).output(0) - median_eval = ov_opset.reshape(median_eval, median_shape, False).output(0) - elif int_axis == True: - median_shape = ov_opset.shape_of(median_eval).output(0) - median_shape = ov_opset.unsqueeze(median_shape, axis).output(0) - median_eval = ov_opset.reshape(median_eval, median_shape, False).output(0) + + if keepdims: + if flattened: + median_shape = ov_opset.divide( + x_shape_original, x_shape_original, "none" + ).output(0) + median_eval = ov_opset.reshape( + median_eval, median_shape, False + ).output(0) else: median_eval = ov_opset.unsqueeze(median_eval, ov_axis).output(0) - + return OpenVINOKerasTensor(median_eval) + def meshgrid(*x, indexing="xy"): raise NotImplementedError( "`meshgrid` is not supported with openvino backend" From 9fd5282f25d1be5e722ae372fb1f7eb0b6fc1a37 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 12:02:55 +0800 Subject: [PATCH 03/21] Added comments --- keras/src/backend/openvino/numpy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index 01df0cccb43c..cc89369e59ea 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1076,6 +1076,8 @@ def median(x, axis=None, keepdims=False): x_shape, ov_axis, ov_opset.constant([0], Type.i32).output(0) ).output(0) else: + # where axis is tuple or list of integers, move 'axis' dims to the + # rightmost positions and flatten them flattened = False ov_axis = get_ov_output(axis) x_rank = ov_opset.gather( @@ -1127,6 +1129,8 @@ def median(x, axis=None, keepdims=False): ov_opset.constant([0], Type.i32).output(0), ).output(0) + # negative axis values are incompatible with ov_opset.gather axis arguement, + # convert the values if axis < 0: x_rank = ov_opset.gather( ov_opset.shape_of(x, Type.i32).output(0), From 93797bed5e74287ca1b929c3714be27685351e2b Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 12:46:50 +0800 Subject: [PATCH 04/21] Fixed k_value not scalar --- keras/src/backend/openvino/numpy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index cc89369e59ea..d516c826e129 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1147,8 +1147,9 @@ def median(x, axis=None, keepdims=False): else: ov_axis_positive = ov_axis + k_scalar = ov_opset.squeeze(k_value).output(0) x_sorted = ov_opset.topk( - x, k_value, axis, "min", "value", stable=True + x, k_scalar, axis, "min", "value", stable=True ).output(0) half_index = ov_opset.floor( ov_opset.divide(k_value, ov_opset.constant([2], Type.i32)).output(0) From 97e38520f707e12ba15a04d85c144f5cb69fbb77 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 14:07:38 +0800 Subject: [PATCH 05/21] Fixed squeeze missing axis --- keras/src/backend/openvino/numpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index d516c826e129..e8fae69fd66d 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1147,7 +1147,7 @@ def median(x, axis=None, keepdims=False): else: ov_axis_positive = ov_axis - k_scalar = ov_opset.squeeze(k_value).output(0) + k_scalar = ov_opset.squeeze(k_value, ov_opset.constant([0], Type.i32).output(0)).output(0) x_sorted = ov_opset.topk( x, k_scalar, axis, "min", "value", stable=True ).output(0) From 4922cee3af2b92ae2a738f7231ea646956e5f55c Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 14:23:09 +0800 Subject: [PATCH 06/21] Fixed missing output and dtype issues --- keras/src/backend/openvino/numpy.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index e8fae69fd66d..007b7146e387 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1147,7 +1147,9 @@ def median(x, axis=None, keepdims=False): else: ov_axis_positive = ov_axis - k_scalar = ov_opset.squeeze(k_value, ov_opset.constant([0], Type.i32).output(0)).output(0) + k_scalar = ov_opset.squeeze( + k_value, ov_opset.constant([0], Type.i32).output(0) + ).output(0) x_sorted = ov_opset.topk( x, k_scalar, axis, "min", "value", stable=True ).output(0) @@ -1172,12 +1174,13 @@ def median(x, axis=None, keepdims=False): ).output(0) median_odd = med_0 + median_type = med_0.get_element_type() median_even = ov_opset.divide( ov_opset.add(med_1, med_0).output(0), - ov_opset.constant([2], Type.f32), - ) + ov_opset.constant([2], median_type), + ).output(0) - median_eval = ov_opset.select(is_even, median_even, median_odd) + median_eval = ov_opset.select(is_even, median_even, median_odd).output(0) if keepdims: if flattened: From 3f56a13ad762dd00c6c93f2398df05045be4d309 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 14:41:34 +0800 Subject: [PATCH 07/21] Fixed final median shape issue --- keras/src/backend/openvino/numpy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index 007b7146e387..a77452df0a23 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1192,6 +1192,8 @@ def median(x, axis=None, keepdims=False): ).output(0) else: median_eval = ov_opset.unsqueeze(median_eval, ov_axis).output(0) + else: + median_eval = ov_opset.squeeze(median_eval, ov_axis_positive).output(0) return OpenVINOKerasTensor(median_eval) From 77f7859ff045ae112cd338ccd80496f2cb95b4ed Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 15:08:00 +0800 Subject: [PATCH 08/21] Fix tuple convert to OpenVINO --- keras/src/backend/openvino/numpy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index a77452df0a23..8bc6432a752e 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1079,7 +1079,10 @@ def median(x, axis=None, keepdims=False): # where axis is tuple or list of integers, move 'axis' dims to the # rightmost positions and flatten them flattened = False - ov_axis = get_ov_output(axis) + if isinstance(axis, (tuple, list)): + ov_axis = convert_to_tensor(axis) + else: + ov_axis = get_ov_output(axis) x_rank = ov_opset.gather( ov_opset.shape_of(x_shape_original, Type.i32).output(0), ov_opset.constant([0], Type.i32).output(0), From fbb727a6aaacffdf389a46c221390eb8f4320c02 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 15:18:23 +0800 Subject: [PATCH 09/21] Fix missing gather axis arguements --- keras/src/backend/openvino/numpy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index 8bc6432a752e..5e3b1b1284ef 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1086,6 +1086,7 @@ def median(x, axis=None, keepdims=False): x_rank = ov_opset.gather( ov_opset.shape_of(x_shape_original, Type.i32).output(0), ov_opset.constant([0], Type.i32).output(0), + ov_opset.constant([0], Type.i32).output(0), ).output(0) axis_as_range = ov_opset.range( ov_opset.constant([0], Type.i32).output(0), @@ -1138,6 +1139,7 @@ def median(x, axis=None, keepdims=False): x_rank = ov_opset.gather( ov_opset.shape_of(x, Type.i32).output(0), ov_opset.constant([0], Type.i32).output(0), + ov_opset.constant([0], Type.i32).output(0), ).output(0) axis_as_range = ov_opset.range( ov_opset.constant([0], Type.i32).output(0), From e66b94383addac906048f3081f797c74ced69c89 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 15:28:46 +0800 Subject: [PATCH 10/21] Fix missing range dtype --- keras/src/backend/openvino/numpy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index 5e3b1b1284ef..f6450f58b592 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1092,6 +1092,7 @@ def median(x, axis=None, keepdims=False): ov_opset.constant([0], Type.i32).output(0), x_rank, ov_opset.constant([1], Type.i32).output(0), + "i32", ).output(0) axis_compare = ov_opset.not_equal( ov_opset.unsqueeze(axis_as_range, 1).output(0), @@ -1145,6 +1146,7 @@ def median(x, axis=None, keepdims=False): ov_opset.constant([0], Type.i32).output(0), x_rank, ov_opset.constant([1], Type.i32).output(0), + "i32", ).output(0) ov_axis_positive = ov_opset.gather( axis_as_range, ov_axis, ov_opset.constant([0], Type.i32) From 45ed5bf5a2b7ace01662350693cab0e9e8bde9e8 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 15:44:12 +0800 Subject: [PATCH 11/21] Fix range start is scalar --- keras/src/backend/openvino/numpy.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index f6450f58b592..b5e08a5d10c7 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1088,10 +1088,13 @@ def median(x, axis=None, keepdims=False): ov_opset.constant([0], Type.i32).output(0), ov_opset.constant([0], Type.i32).output(0), ).output(0) + x_rank_scalar = ov_opset.squeeze( + x_rank, ov_opset.constant([0], Type.i32).output(0) + ).output(0) axis_as_range = ov_opset.range( - ov_opset.constant([0], Type.i32).output(0), - x_rank, - ov_opset.constant([1], Type.i32).output(0), + ov_opset.constant(0, Type.i32).output(0), + x_rank_scalar, + ov_opset.constant(1, Type.i32).output(0), "i32", ).output(0) axis_compare = ov_opset.not_equal( @@ -1142,10 +1145,13 @@ def median(x, axis=None, keepdims=False): ov_opset.constant([0], Type.i32).output(0), ov_opset.constant([0], Type.i32).output(0), ).output(0) + x_rank_scalar = ov_opset.squeeze( + x_rank, ov_opset.constant([0], Type.i32).output(0) + ).output(0) axis_as_range = ov_opset.range( - ov_opset.constant([0], Type.i32).output(0), - x_rank, - ov_opset.constant([1], Type.i32).output(0), + ov_opset.constant(0, Type.i32).output(0), + x_rank_scalar, + ov_opset.constant(1, Type.i32).output(0), "i32", ).output(0) ov_axis_positive = ov_opset.gather( From 073a48a8a0a41413ec80024cad7191d2b3e80f79 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 16:24:19 +0800 Subject: [PATCH 12/21] NumpyDtypeTest::test_median reinserted --- keras/src/backend/openvino/excluded_concrete_tests.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/keras/src/backend/openvino/excluded_concrete_tests.txt b/keras/src/backend/openvino/excluded_concrete_tests.txt index bbb0ae427bf9..e8ce6f759685 100644 --- a/keras/src/backend/openvino/excluded_concrete_tests.txt +++ b/keras/src/backend/openvino/excluded_concrete_tests.txt @@ -38,6 +38,7 @@ NumpyDtypeTest::test_logspace NumpyDtypeTest::test_matmul_ NumpyDtypeTest::test_max NumpyDtypeTest::test_mean +NumpyDtypeTest::test_median NumpyDtypeTest::test_meshgrid NumpyDtypeTest::test_minimum_python_types NumpyDtypeTest::test_multiply From 2a926e94e307a558bffa5500259c66e5c4616451 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 16:32:29 +0800 Subject: [PATCH 13/21] Revert to state of 'Fix missing range dtype' --- .../openvino/excluded_concrete_tests.txt | 1 - keras/src/backend/openvino/numpy.py | 18 ++++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/keras/src/backend/openvino/excluded_concrete_tests.txt b/keras/src/backend/openvino/excluded_concrete_tests.txt index e8ce6f759685..bbb0ae427bf9 100644 --- a/keras/src/backend/openvino/excluded_concrete_tests.txt +++ b/keras/src/backend/openvino/excluded_concrete_tests.txt @@ -38,7 +38,6 @@ NumpyDtypeTest::test_logspace NumpyDtypeTest::test_matmul_ NumpyDtypeTest::test_max NumpyDtypeTest::test_mean -NumpyDtypeTest::test_median NumpyDtypeTest::test_meshgrid NumpyDtypeTest::test_minimum_python_types NumpyDtypeTest::test_multiply diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index b5e08a5d10c7..f6450f58b592 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1088,13 +1088,10 @@ def median(x, axis=None, keepdims=False): ov_opset.constant([0], Type.i32).output(0), ov_opset.constant([0], Type.i32).output(0), ).output(0) - x_rank_scalar = ov_opset.squeeze( - x_rank, ov_opset.constant([0], Type.i32).output(0) - ).output(0) axis_as_range = ov_opset.range( - ov_opset.constant(0, Type.i32).output(0), - x_rank_scalar, - ov_opset.constant(1, Type.i32).output(0), + ov_opset.constant([0], Type.i32).output(0), + x_rank, + ov_opset.constant([1], Type.i32).output(0), "i32", ).output(0) axis_compare = ov_opset.not_equal( @@ -1145,13 +1142,10 @@ def median(x, axis=None, keepdims=False): ov_opset.constant([0], Type.i32).output(0), ov_opset.constant([0], Type.i32).output(0), ).output(0) - x_rank_scalar = ov_opset.squeeze( - x_rank, ov_opset.constant([0], Type.i32).output(0) - ).output(0) axis_as_range = ov_opset.range( - ov_opset.constant(0, Type.i32).output(0), - x_rank_scalar, - ov_opset.constant(1, Type.i32).output(0), + ov_opset.constant([0], Type.i32).output(0), + x_rank, + ov_opset.constant([1], Type.i32).output(0), "i32", ).output(0) ov_axis_positive = ov_opset.gather( From 916faedcd6fd23f5520323842d1d196a7bec783c Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 16:45:14 +0800 Subject: [PATCH 14/21] 2nd fix for range start with scalar --- keras/src/backend/openvino/numpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index f6450f58b592..a7d7e370b26a 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1089,7 +1089,7 @@ def median(x, axis=None, keepdims=False): ov_opset.constant([0], Type.i32).output(0), ).output(0) axis_as_range = ov_opset.range( - ov_opset.constant([0], Type.i32).output(0), + ov_opset.constant(0, Type.i32).output(0), x_rank, ov_opset.constant([1], Type.i32).output(0), "i32", @@ -1143,7 +1143,7 @@ def median(x, axis=None, keepdims=False): ov_opset.constant([0], Type.i32).output(0), ).output(0) axis_as_range = ov_opset.range( - ov_opset.constant([0], Type.i32).output(0), + ov_opset.constant(0, Type.i32).output(0), x_rank, ov_opset.constant([1], Type.i32).output(0), "i32", From 69268d9e6cf7ca333cb38952fd9cf5e459e4423f Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Fri, 13 Jun 2025 17:00:29 +0800 Subject: [PATCH 15/21] Fix missing rank scalar --- keras/src/backend/openvino/numpy.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index a7d7e370b26a..8b2a277d196f 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1088,10 +1088,13 @@ def median(x, axis=None, keepdims=False): ov_opset.constant([0], Type.i32).output(0), ov_opset.constant([0], Type.i32).output(0), ).output(0) + x_rank_scalar = ov_opset.squeeze( + x_rank, ov_opset.constant([0], Type.i32).output(0) + ).output(0) axis_as_range = ov_opset.range( ov_opset.constant(0, Type.i32).output(0), - x_rank, - ov_opset.constant([1], Type.i32).output(0), + x_rank_scalar, + ov_opset.constant(1, Type.i32).output(0), "i32", ).output(0) axis_compare = ov_opset.not_equal( @@ -1137,15 +1140,19 @@ def median(x, axis=None, keepdims=False): # negative axis values are incompatible with ov_opset.gather axis arguement, # convert the values if axis < 0: + x_shape = ov_opset.shape_of(x, Type.i32).output(0) x_rank = ov_opset.gather( - ov_opset.shape_of(x, Type.i32).output(0), + ov_opset.shape_of(x_shape, Type.i32).output(0), ov_opset.constant([0], Type.i32).output(0), ov_opset.constant([0], Type.i32).output(0), ).output(0) + x_rank_scalar = ov_opset.squeeze( + x_rank, ov_opset.constant([0], Type.i32).output(0) + ).output(0) axis_as_range = ov_opset.range( ov_opset.constant(0, Type.i32).output(0), - x_rank, - ov_opset.constant([1], Type.i32).output(0), + x_rank_scalar, + ov_opset.constant(1, Type.i32).output(0), "i32", ).output(0) ov_axis_positive = ov_opset.gather( From d92b60a9651faf4d49ca62e5b651d9265bb45033 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Wed, 18 Jun 2025 13:23:28 +0800 Subject: [PATCH 16/21] fixed and passed local testing. Submit median for PR --- keras/src/backend/openvino/numpy.py | 165 ++++++++++++---------------- 1 file changed, 70 insertions(+), 95 deletions(-) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index 8b2a277d196f..727cfd048957 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1052,127 +1052,101 @@ def median(x, axis=None, keepdims=False): x = get_ov_output(x) x_type = x.get_element_type() + x_rank_org = x.get_partial_shape().rank.get_length() if x_type == Type.boolean or x_type.is_integral(): - x = ov_opset.convert(x, Type.f32).output(0) - x_type = x.get_element_type() + x_type = OPENVINO_DTYPES[config.floatx()] + x = ov_opset.convert(x, x_type).output(0) + x_shape_original = ov_opset.shape_of(x, Type.i32).output(0) if axis is None: flatten_shape = ov_opset.constant([-1], Type.i32).output(0) x = ov_opset.reshape(x, flatten_shape, False).output(0) axis = 0 - ov_axis = get_ov_output(axis) + axis_norm = axis + ov_axis_positive = get_ov_output(axis) flattened = True - k_value = ov_opset.gather( - ov_opset.shape_of(x, Type.i32).output(0), - ov_opset.constant([0], Type.i32).output(0), - ov_axis, - ).output(0) + k_value = x.get_partial_shape().get_dimension(index=0).get_length() elif isinstance(axis, int): flattened = False - ov_axis = get_ov_output(axis) - x_shape = ov_opset.shape_of(x, Type.i32).output(0) - k_value = ov_opset.gather( - x_shape, ov_axis, ov_opset.constant([0], Type.i32).output(0) - ).output(0) + x_rank = x.get_partial_shape().rank.get_length() + if axis < 0: + axis_norm = x_rank + axis + else: + axis_norm = axis + ov_axis_positive = ov_axis = get_ov_output(axis) + k_value = ( + x.get_partial_shape().get_dimension(index=axis_norm).get_length() + ) else: # where axis is tuple or list of integers, move 'axis' dims to the # rightmost positions and flatten them flattened = False if isinstance(axis, (tuple, list)): - ov_axis = convert_to_tensor(axis) - else: - ov_axis = get_ov_output(axis) - x_rank = ov_opset.gather( - ov_opset.shape_of(x_shape_original, Type.i32).output(0), - ov_opset.constant([0], Type.i32).output(0), - ov_opset.constant([0], Type.i32).output(0), - ).output(0) - x_rank_scalar = ov_opset.squeeze( - x_rank, ov_opset.constant([0], Type.i32).output(0) - ).output(0) + ov_axis = axis = list(axis) + ov_axis = ov_opset.constant(axis, Type.i32).output(0) + x_rank = x.get_partial_shape().rank.get_length() axis_as_range = ov_opset.range( ov_opset.constant(0, Type.i32).output(0), - x_rank_scalar, + x_rank, ov_opset.constant(1, Type.i32).output(0), - "i32", - ).output(0) - axis_compare = ov_opset.not_equal( - ov_opset.unsqueeze(axis_as_range, 1).output(0), - ov_opset.unsqueeze(ov_axis, 0).output(0), - "NUMPY", - ).output(0) - keep_axes = ov_opset.reduce_logical_or( - axis_compare, ov_opset.constant([1], Type.i32).output(0) + Type.i32, ).output(0) - nz = ov_opset.non_zero(keep_axes, Type.i32).output(0) - keep_axes = ov_opset.reduce_sum( - nz, ov_opset.constant([1], Type.i32).output(0) - ).output(0) - reordered_axes = ov_opset.concat( - [keep_axes, ov_axis], ov_opset.constant([0], Type.i32).output(0) + # normalise any negative axes to their positive indices + ov_axis_positive = ov_opset.gather( + axis_as_range, ov_axis, ov_opset.constant([0], Type.i32) ).output(0) - x = ov_opset.transpose(x, reordered_axes).output(0) + # only move axis dims if tuple contains more than 1 axis + if ov_axis_positive.get_partial_shape().rank.get_length() > 1: + axis_compare = ov_opset.not_equal( + ov_opset.unsqueeze(axis_as_range, 1).output(0), + ov_opset.unsqueeze(ov_axis_positive, 0).output(0), + ).output(0) + keep_axes = ov_opset.reduce_logical_or( + axis_compare, ov_opset.constant([1], Type.i32).output(0) + ).output(0) + nz = ov_opset.non_zero(keep_axes, Type.i32).output(0) + keep_axes = ov_opset.reduce_sum( + nz, ov_opset.constant([1], Type.i32).output(0) + ).output(0) + reordered_axes = ov_opset.concat( + [keep_axes, ov_axis_positive], 0 + ).output(0) + x = ov_opset.transpose(x, reordered_axes).output(0) - flat_rank = ov_opset.subtract( - x_rank, ov_opset.constant([1], Type.i32) - ).output(0) - flatten_shape = ov_opset.broadcast( - ov_opset.constant([0], Type.i32).output(0), flat_rank - ).output(0) - flatten_shape = ov_opset.scatter_elements_update( - flatten_shape, - ov_opset.constant([-1], Type.i32).output(0), - ov_opset.constant([-1], Type.i32).output(0), - 0, - "sum", - ).output(0) + flat_rank = ov_opset.subtract( + x_rank, ov_opset.constant([1], Type.i64).output(0) + ).output(0) + flatten_shape = ov_opset.broadcast( + ov_opset.constant([0], Type.i32).output(0), flat_rank + ).output(0) + flatten_shape = ov_opset.scatter_elements_update( + flatten_shape, + ov_opset.constant([-1], Type.i32).output(0), + ov_opset.constant([-1], Type.i32).output(0), + 0, + "sum", + ).output(0) - x = ov_opset.reshape(x, flatten_shape, True).output(0) + x = ov_opset.reshape(x, flatten_shape, True).output(0) axis = -1 - x_shape = ov_opset.shape_of(x, Type.i32).output(0) - k_value = ov_opset.gather( - x_shape, - ov_opset.constant([-1], Type.i32).output(0), - ov_opset.constant([0], Type.i32).output(0), - ).output(0) - - # negative axis values are incompatible with ov_opset.gather axis arguement, - # convert the values - if axis < 0: - x_shape = ov_opset.shape_of(x, Type.i32).output(0) - x_rank = ov_opset.gather( - ov_opset.shape_of(x_shape, Type.i32).output(0), - ov_opset.constant([0], Type.i32).output(0), - ov_opset.constant([0], Type.i32).output(0), - ).output(0) - x_rank_scalar = ov_opset.squeeze( - x_rank, ov_opset.constant([0], Type.i32).output(0) - ).output(0) - axis_as_range = ov_opset.range( - ov_opset.constant(0, Type.i32).output(0), - x_rank_scalar, - ov_opset.constant(1, Type.i32).output(0), - "i32", - ).output(0) - ov_axis_positive = ov_opset.gather( - axis_as_range, ov_axis, ov_opset.constant([0], Type.i32) - ).output(0) - else: - ov_axis_positive = ov_axis + x_rank = x.get_partial_shape().rank.get_length() + axis_norm = x_rank + axis + ov_axis_positive = get_ov_output(axis_norm) + k_value = ( + x.get_partial_shape().get_dimension(index=axis_norm).get_length() + ) - k_scalar = ov_opset.squeeze( - k_value, ov_opset.constant([0], Type.i32).output(0) - ).output(0) x_sorted = ov_opset.topk( - x, k_scalar, axis, "min", "value", stable=True + x, k_value, axis_norm, "min", "value", stable=True ).output(0) + k_value = ov_opset.convert(k_value, x_type).output(0) half_index = ov_opset.floor( - ov_opset.divide(k_value, ov_opset.constant([2], Type.i32)).output(0) + ov_opset.divide(k_value, ov_opset.constant([2], x_type)).output(0) ).output(0) half_index = ov_opset.convert(half_index, Type.i32).output(0) - x_mod = ov_opset.mod(k_value, ov_opset.constant([2], Type.i32)).output(0) - is_even = ov_opset.equal(x_mod, ov_opset.constant([0], Type.i32)).output(0) + x_mod = ov_opset.mod(k_value, ov_opset.constant([2], x_type)).output(0) + is_even = ov_opset.equal(x_mod, ov_opset.constant([0], x_type)).output(0) med_0 = ov_opset.gather(x_sorted, half_index, ov_axis_positive).output(0) med_1 = ov_opset.select( @@ -1188,10 +1162,9 @@ def median(x, axis=None, keepdims=False): ).output(0) median_odd = med_0 - median_type = med_0.get_element_type() median_even = ov_opset.divide( ov_opset.add(med_1, med_0).output(0), - ov_opset.constant([2], median_type), + ov_opset.constant([2], x_type), ).output(0) median_eval = ov_opset.select(is_even, median_even, median_odd).output(0) @@ -1205,7 +1178,9 @@ def median(x, axis=None, keepdims=False): median_eval, median_shape, False ).output(0) else: - median_eval = ov_opset.unsqueeze(median_eval, ov_axis).output(0) + if median_eval.get_partial_shape().rank.get_length() != x_rank_org: + median_eval = ov_opset.unsqueeze(median_eval, ov_axis).output(0) + else: median_eval = ov_opset.squeeze(median_eval, ov_axis_positive).output(0) From 77dc3264729a89d9505f1af01bc3629860feb265 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Sun, 13 Jul 2025 14:55:18 +0100 Subject: [PATCH 17/21] test --- keras/src/backend/openvino/numpy.py | 164 ++++++++++++++++------------ 1 file changed, 94 insertions(+), 70 deletions(-) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index 727cfd048957..2790df15a390 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -545,6 +545,10 @@ def broadcast_to(x, shape): return OpenVINOKerasTensor(ov_opset.broadcast(x, target_shape).output(0)) +def cbrt(x): + raise NotImplementedError("`cbrt` is not supported with openvino backend") + + def ceil(x): x = get_ov_output(x) return OpenVINOKerasTensor(ov_opset.ceil(x).output(0)) @@ -642,6 +646,12 @@ def cumsum(x, axis=None, dtype=None): return OpenVINOKerasTensor(ov_opset.cumsum(x, axis).output(0)) +def deg2rad(x): + raise NotImplementedError( + "`deg2rad` is not supported with openvino backend" + ) + + def diag(x, k=0): raise NotImplementedError("`diag` is not supported with openvino backend") @@ -1052,43 +1062,33 @@ def median(x, axis=None, keepdims=False): x = get_ov_output(x) x_type = x.get_element_type() - x_rank_org = x.get_partial_shape().rank.get_length() if x_type == Type.boolean or x_type.is_integral(): x_type = OPENVINO_DTYPES[config.floatx()] x = ov_opset.convert(x, x_type).output(0) x_shape_original = ov_opset.shape_of(x, Type.i32).output(0) + x_rank_org = ov_opset.shape_of(x_shape_original, Type.i32).output(0) + x_rank_org_scalar = ov_opset.squeeze( + x_rank_org, ov_opset.constant(0, Type.i32).output(0) + ).output(0) if axis is None: flatten_shape = ov_opset.constant([-1], Type.i32).output(0) x = ov_opset.reshape(x, flatten_shape, False).output(0) - axis = 0 - axis_norm = axis - ov_axis_positive = get_ov_output(axis) + ov_axis_positive = ov_opset.constant([0], Type.i32).output(0) flattened = True - k_value = x.get_partial_shape().get_dimension(index=0).get_length() - elif isinstance(axis, int): - flattened = False - x_rank = x.get_partial_shape().rank.get_length() - if axis < 0: - axis_norm = x_rank + axis - else: - axis_norm = axis - ov_axis_positive = ov_axis = get_ov_output(axis) - k_value = ( - x.get_partial_shape().get_dimension(index=axis_norm).get_length() - ) + else: - # where axis is tuple or list of integers, move 'axis' dims to the - # rightmost positions and flatten them + # move 'axis' dims to the rightmost positions flattened = False + if isinstance(axis, int): + axis = [axis] if isinstance(axis, (tuple, list)): - ov_axis = axis = list(axis) + axis = list(axis) ov_axis = ov_opset.constant(axis, Type.i32).output(0) - x_rank = x.get_partial_shape().rank.get_length() axis_as_range = ov_opset.range( ov_opset.constant(0, Type.i32).output(0), - x_rank, + x_rank_org_scalar, ov_opset.constant(1, Type.i32).output(0), Type.i32, ).output(0) @@ -1096,67 +1096,89 @@ def median(x, axis=None, keepdims=False): ov_axis_positive = ov_opset.gather( axis_as_range, ov_axis, ov_opset.constant([0], Type.i32) ).output(0) - # only move axis dims if tuple contains more than 1 axis - if ov_axis_positive.get_partial_shape().rank.get_length() > 1: - axis_compare = ov_opset.not_equal( - ov_opset.unsqueeze(axis_as_range, 1).output(0), - ov_opset.unsqueeze(ov_axis_positive, 0).output(0), - ).output(0) - keep_axes = ov_opset.reduce_logical_or( - axis_compare, ov_opset.constant([1], Type.i32).output(0) - ).output(0) - nz = ov_opset.non_zero(keep_axes, Type.i32).output(0) - keep_axes = ov_opset.reduce_sum( - nz, ov_opset.constant([1], Type.i32).output(0) - ).output(0) - reordered_axes = ov_opset.concat( - [keep_axes, ov_axis_positive], 0 - ).output(0) - x = ov_opset.transpose(x, reordered_axes).output(0) - flat_rank = ov_opset.subtract( - x_rank, ov_opset.constant([1], Type.i64).output(0) + # reshape axis tensors and compare to seperate the flatten and keep axes + axis_comparison_shape = ov_opset.concat( + [ + ov_opset.shape_of(ov_axis_positive, Type.i32).output(0), + ov_opset.shape_of(axis_as_range, Type.i32).output(0), + ], + 0, + ).output(0) + reshaped_axis_range = ov_opset.broadcast( + axis_as_range, axis_comparison_shape + ).output(0) + axis_compare = ov_opset.not_equal( + reshaped_axis_range, + ov_opset.unsqueeze( + ov_axis_positive, ov_opset.constant(1, Type.i32).output(0) + ).output(0), + ).output(0) + axis_compare = ov_opset.reduce_logical_and( + axis_compare, ov_opset.constant(0, Type.i32).output(0) + ).output(0) + nz = ov_opset.non_zero(axis_compare, Type.i32).output(0) + nz = ov_opset.squeeze( + nz, ov_opset.constant(0, Type.i32).output(0) + ).output(0) + keep_axes = ov_opset.gather( + axis_as_range, nz, ov_opset.constant(0, Type.i32).output(0) + ).output(0) + # concat to place keep axes on the left and flatten axes on the right + reordered_axes = ov_opset.concat( + [keep_axes, ov_axis_positive], 0 + ).output(0) + x_transposed = ov_opset.transpose(x, reordered_axes).output(0) + + # flatten the axis dims if more than 1 axis in input + if len(axis) > 1: + x_flatten_rank = ov_opset.subtract( + x_rank_org, ov_opset.constant([1], Type.i32).output(0) ).output(0) - flatten_shape = ov_opset.broadcast( - ov_opset.constant([0], Type.i32).output(0), flat_rank + x_flatten_shape = ov_opset.broadcast( + ov_opset.constant([0], Type.i32).output(0), x_flatten_rank ).output(0) - flatten_shape = ov_opset.scatter_elements_update( - flatten_shape, + x_flatten_shape = ov_opset.scatter_elements_update( + x_flatten_shape, ov_opset.constant([-1], Type.i32).output(0), ov_opset.constant([-1], Type.i32).output(0), 0, "sum", ).output(0) - x = ov_opset.reshape(x, flatten_shape, True).output(0) - axis = -1 - x_rank = x.get_partial_shape().rank.get_length() - axis_norm = x_rank + axis - ov_axis_positive = get_ov_output(axis_norm) - k_value = ( - x.get_partial_shape().get_dimension(index=axis_norm).get_length() - ) + x_transposed = ov_opset.reshape( + x_transposed, x_flatten_shape, True + ).output(0) + + x = x_transposed + + k_value = ov_opset.gather( + ov_opset.shape_of(x, Type.i32).output(0), + ov_opset.constant(-1, Type.i32).output(0), + ov_opset.constant(0, Type.i32).output(0), + ).output(0) x_sorted = ov_opset.topk( - x, k_value, axis_norm, "min", "value", stable=True + x, k_value, -1, "min", "value", stable=True ).output(0) - k_value = ov_opset.convert(k_value, x_type).output(0) + half_index = ov_opset.floor( - ov_opset.divide(k_value, ov_opset.constant([2], x_type)).output(0) + ov_opset.divide(k_value, ov_opset.constant(2, Type.i32)).output(0) ).output(0) - half_index = ov_opset.convert(half_index, Type.i32).output(0) - x_mod = ov_opset.mod(k_value, ov_opset.constant([2], x_type)).output(0) - is_even = ov_opset.equal(x_mod, ov_opset.constant([0], x_type)).output(0) + x_mod = ov_opset.mod(k_value, ov_opset.constant(2, Type.i32)).output(0) + is_even = ov_opset.equal(x_mod, ov_opset.constant(0, Type.i32)).output(0) - med_0 = ov_opset.gather(x_sorted, half_index, ov_axis_positive).output(0) + med_0 = ov_opset.gather( + x_sorted, half_index, ov_opset.constant(-1, Type.i32).output(0) + ).output(0) med_1 = ov_opset.select( is_even, ov_opset.gather( x_sorted, ov_opset.subtract( - half_index, ov_opset.constant([1], Type.i32) + half_index, ov_opset.constant(1, Type.i32) ).output(0), - ov_axis_positive, + ov_opset.constant(-1, Type.i32).output(0), ).output(0), med_0, ).output(0) @@ -1164,7 +1186,7 @@ def median(x, axis=None, keepdims=False): median_odd = med_0 median_even = ov_opset.divide( ov_opset.add(med_1, med_0).output(0), - ov_opset.constant([2], x_type), + ov_opset.constant(2, x_type), ).output(0) median_eval = ov_opset.select(is_even, median_even, median_odd).output(0) @@ -1178,11 +1200,9 @@ def median(x, axis=None, keepdims=False): median_eval, median_shape, False ).output(0) else: - if median_eval.get_partial_shape().rank.get_length() != x_rank_org: - median_eval = ov_opset.unsqueeze(median_eval, ov_axis).output(0) - - else: - median_eval = ov_opset.squeeze(median_eval, ov_axis_positive).output(0) + median_eval = ov_opset.unsqueeze( + median_eval, ov_axis_positive + ).output(0) return OpenVINOKerasTensor(median_eval) @@ -1305,9 +1325,9 @@ def nan_to_num(x, nan=0.0, posinf=None, neginf=None): def ndim(x): x = get_ov_output(x) - x_shape = ov_opset.shape_of(x).output(0) - x_dim = ov_opset.shape_of(x_shape, "i64") - return x_dim + shape_tensor = ov_opset.shape_of(x, Type.i64).output(0) + rank_tensor = ov_opset.shape_of(shape_tensor, Type.i64).output(0) + return OpenVINOKerasTensor(rank_tensor) def nonzero(x): @@ -1782,6 +1802,10 @@ def var(x, axis=None, keepdims=False): def sum(x, axis=None, keepdims=False): x = get_ov_output(x) + if axis is None: + flatten_shape = ov_opset.constant([-1], Type.i32).output(0) + x = ov_opset.reshape(x, flatten_shape, False).output(0) + axis = 0 axis = ov_opset.constant(axis, Type.i32).output(0) return OpenVINOKerasTensor(ov_opset.reduce_sum(x, axis, keepdims).output(0)) From 4e74f2e414c66edd6ac54797d679c923d7d92b50 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Sun, 13 Jul 2025 16:36:46 +0100 Subject: [PATCH 18/21] fixed x_flatten_rank calculation --- keras/src/backend/openvino/numpy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index 2790df15a390..4a50159aceae 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1133,7 +1133,8 @@ def median(x, axis=None, keepdims=False): # flatten the axis dims if more than 1 axis in input if len(axis) > 1: x_flatten_rank = ov_opset.subtract( - x_rank_org, ov_opset.constant([1], Type.i32).output(0) + x_rank_org, + ov_opset.constant([len(axis) - 1], Type.i32).output(0), ).output(0) x_flatten_shape = ov_opset.broadcast( ov_opset.constant([0], Type.i32).output(0), x_flatten_rank From 8786cfcb340665e364b02abf94d24ceae0654d13 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Sun, 13 Jul 2025 17:10:00 +0100 Subject: [PATCH 19/21] added comments following review --- keras/src/backend/openvino/numpy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index 4a50159aceae..2d53d5430081 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1092,7 +1092,8 @@ def median(x, axis=None, keepdims=False): ov_opset.constant(1, Type.i32).output(0), Type.i32, ).output(0) - # normalise any negative axes to their positive indices + # normalise any negative axes to their positive equivalents by gathering + # the indices from axis range ov_axis_positive = ov_opset.gather( axis_as_range, ov_axis, ov_opset.constant([0], Type.i32) ).output(0) @@ -1194,6 +1195,7 @@ def median(x, axis=None, keepdims=False): if keepdims: if flattened: + # create a tensor of ones, length matching original rank of x median_shape = ov_opset.divide( x_shape_original, x_shape_original, "none" ).output(0) From 04e72cf873201ae1070f63693399ccfa77f74abe Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Mon, 14 Jul 2025 11:28:27 +0100 Subject: [PATCH 20/21] comments added and variables renamed to improve clarity. med_1 calculation simplified --- keras/src/backend/openvino/numpy.py | 73 +++++++++++++++++------------ pytest.ini | 3 -- 2 files changed, 42 insertions(+), 34 deletions(-) delete mode 100644 pytest.ini diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index 2d53d5430081..d45681feed96 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1060,6 +1060,13 @@ def median(x, axis=None, keepdims=False): x = get_ov_output(x) return OpenVINOKerasTensor(x) + # the median algorithm follows numpy's method; + # if axis is None, flatten all dimensions of the array and find the + # median value. + # if axis is single int or list/tuple of multiple values, re-order x array + # to move those axis dims to the right, flatten the multiple axis dims + # then calculate median values along the flattened axis. + x = get_ov_output(x) x_type = x.get_element_type() if x_type == Type.boolean or x_type.is_integral(): @@ -1067,41 +1074,46 @@ def median(x, axis=None, keepdims=False): x = ov_opset.convert(x, x_type).output(0) x_shape_original = ov_opset.shape_of(x, Type.i32).output(0) - x_rank_org = ov_opset.shape_of(x_shape_original, Type.i32).output(0) - x_rank_org_scalar = ov_opset.squeeze( - x_rank_org, ov_opset.constant(0, Type.i32).output(0) + x_rank_original = ov_opset.shape_of(x_shape_original, Type.i32).output(0) + x_rank_original_scalar = ov_opset.squeeze( + x_rank_original, ov_opset.constant(0, Type.i32).output(0) ).output(0) if axis is None: flatten_shape = ov_opset.constant([-1], Type.i32).output(0) x = ov_opset.reshape(x, flatten_shape, False).output(0) - ov_axis_positive = ov_opset.constant([0], Type.i32).output(0) flattened = True else: - # move 'axis' dims to the rightmost positions + # move axis dims to the rightmost positions. flattened = False if isinstance(axis, int): axis = [axis] if isinstance(axis, (tuple, list)): axis = list(axis) ov_axis = ov_opset.constant(axis, Type.i32).output(0) + # normalise any negative axes to their positive equivalents by gathering + # the indices from axis range. axis_as_range = ov_opset.range( ov_opset.constant(0, Type.i32).output(0), - x_rank_org_scalar, + x_rank_original_scalar, ov_opset.constant(1, Type.i32).output(0), Type.i32, ).output(0) - # normalise any negative axes to their positive equivalents by gathering - # the indices from axis range - ov_axis_positive = ov_opset.gather( + flatten_axes = ov_opset.gather( axis_as_range, ov_axis, ov_opset.constant([0], Type.i32) ).output(0) - # reshape axis tensors and compare to seperate the flatten and keep axes + # right (flatten) axis dims are defined, + # now define the left (remaining) axis dims. + + # to find remaining axes, use not_equal comparison between flatten_axes + # and axis_as_range. + # reshape axis_as_range to suit not_equal broadcasting rules for + # comparison. axis_comparison_shape = ov_opset.concat( [ - ov_opset.shape_of(ov_axis_positive, Type.i32).output(0), + ov_opset.shape_of(flatten_axes, Type.i32).output(0), ov_opset.shape_of(axis_as_range, Type.i32).output(0), ], 0, @@ -1112,7 +1124,7 @@ def median(x, axis=None, keepdims=False): axis_compare = ov_opset.not_equal( reshaped_axis_range, ov_opset.unsqueeze( - ov_axis_positive, ov_opset.constant(1, Type.i32).output(0) + flatten_axes, ov_opset.constant(1, Type.i32).output(0) ).output(0), ).output(0) axis_compare = ov_opset.reduce_logical_and( @@ -1122,19 +1134,20 @@ def median(x, axis=None, keepdims=False): nz = ov_opset.squeeze( nz, ov_opset.constant(0, Type.i32).output(0) ).output(0) - keep_axes = ov_opset.gather( + remaining_axes = ov_opset.gather( axis_as_range, nz, ov_opset.constant(0, Type.i32).output(0) ).output(0) - # concat to place keep axes on the left and flatten axes on the right + # concat to place flatten axes on the right and remaining axes on the + # left. reordered_axes = ov_opset.concat( - [keep_axes, ov_axis_positive], 0 + [remaining_axes, flatten_axes], 0 ).output(0) x_transposed = ov_opset.transpose(x, reordered_axes).output(0) - # flatten the axis dims if more than 1 axis in input + # flatten the axis dims if more than 1 axis in input. if len(axis) > 1: x_flatten_rank = ov_opset.subtract( - x_rank_org, + x_rank_original, ov_opset.constant([len(axis) - 1], Type.i32).output(0), ).output(0) x_flatten_shape = ov_opset.broadcast( @@ -1167,22 +1180,19 @@ def median(x, axis=None, keepdims=False): half_index = ov_opset.floor( ov_opset.divide(k_value, ov_opset.constant(2, Type.i32)).output(0) ).output(0) + + # for odd length dimension, select the middle value as median. + # for even length dimension, calculate the mean between the 2 middle values. x_mod = ov_opset.mod(k_value, ov_opset.constant(2, Type.i32)).output(0) is_even = ov_opset.equal(x_mod, ov_opset.constant(0, Type.i32)).output(0) med_0 = ov_opset.gather( x_sorted, half_index, ov_opset.constant(-1, Type.i32).output(0) ).output(0) - med_1 = ov_opset.select( - is_even, - ov_opset.gather( - x_sorted, - ov_opset.subtract( - half_index, ov_opset.constant(1, Type.i32) - ).output(0), - ov_opset.constant(-1, Type.i32).output(0), - ).output(0), - med_0, + med_1 = ov_opset.gather( + x_sorted, + ov_opset.subtract(half_index, ov_opset.constant(1, Type.i32)).output(0), + ov_opset.constant(-1, Type.i32).output(0), ).output(0) median_odd = med_0 @@ -1194,8 +1204,9 @@ def median(x, axis=None, keepdims=False): median_eval = ov_opset.select(is_even, median_even, median_odd).output(0) if keepdims: + # reshape median_eval to original rank of x. if flattened: - # create a tensor of ones, length matching original rank of x + # create a tensor of ones for reshape, the original rank of x. median_shape = ov_opset.divide( x_shape_original, x_shape_original, "none" ).output(0) @@ -1203,9 +1214,9 @@ def median(x, axis=None, keepdims=False): median_eval, median_shape, False ).output(0) else: - median_eval = ov_opset.unsqueeze( - median_eval, ov_axis_positive - ).output(0) + median_eval = ov_opset.unsqueeze(median_eval, flatten_axes).output( + 0 + ) return OpenVINOKerasTensor(median_eval) diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 83635a5b7b9b..000000000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -env = - KERAS_BACKEND=openvino \ No newline at end of file From 0eeaa5edcfa15da9cf0ce4b833f3852510fee332 Mon Sep 17 00:00:00 2001 From: Alun Griffith Date: Mon, 14 Jul 2025 11:36:03 +0100 Subject: [PATCH 21/21] added comment to median function --- keras/src/backend/openvino/numpy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/keras/src/backend/openvino/numpy.py b/keras/src/backend/openvino/numpy.py index d45681feed96..83881a15be3f 100644 --- a/keras/src/backend/openvino/numpy.py +++ b/keras/src/backend/openvino/numpy.py @@ -1150,6 +1150,8 @@ def median(x, axis=None, keepdims=False): x_rank_original, ov_opset.constant([len(axis) - 1], Type.i32).output(0), ).output(0) + # create flatten shape of 0's (keep axes) + # and -1 at the end (flattened axis) x_flatten_shape = ov_opset.broadcast( ov_opset.constant([0], Type.i32).output(0), x_flatten_rank ).output(0)