Skip to content

[OpenVINO backend] Support for nanarmax and nanar min #22383

Open
andersendsa wants to merge 2 commits intokeras-team:masterfrom
andersendsa:openvino-nananar-max/min
Open

[OpenVINO backend] Support for nanarmax and nanar min #22383
andersendsa wants to merge 2 commits intokeras-team:masterfrom
andersendsa:openvino-nananar-max/min

Conversation

@andersendsa
Copy link
Contributor

@andersendsa andersendsa commented Mar 8, 2026

Details:

Implement nanarmax and nanarmin operation for the OpenVINO backend.

  • Implemented nanargmax and nanargmin operations: Added support for these functions in the Keras OpenVINO backend (numpy.py) by replacing NaN values with extreme infinities (-inf and inf) and delegating the logic securely to the existing argmax and argmin operators.
  • Enabled test suite coverage: Removed the associated tests from excluded_concrete_tests.txt, allowing the Keras NumPy test suite to verify the implementations.
  • Fixed dynamic axis resolution: Added logic to safely extract integer axis values when they are passed as static OpenVINO Output tensors.

Closes : openvinotoolkit/openvino#34557

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the OpenVINO backend's compatibility with NumPy operations by introducing support for nanargmax and nanargmin. These additions improve the robustness of numerical computations by correctly handling NaN values, and the accompanying fixes for dynamic axis resolution ensure that these and related operations function reliably with OpenVINO's tensor representations. The changes also enable comprehensive testing for these new functionalities.

Highlights

  • Numpy nanargmax and nanargmin Implementation: Implemented nanargmax and nanargmin operations for the OpenVINO backend by replacing NaN values with extreme infinities and delegating logic to existing argmax and argmin operators.
  • Test Suite Coverage Enabled: Removed nanargmax and nanargmin tests from excluded_concrete_tests.txt to enable full test suite coverage for the new implementations.
  • Dynamic Axis Resolution Fixed: Added logic to safely extract integer axis values when they are passed as static OpenVINO Output tensors within _resolve_axis, argmax, and argmin functions.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • keras/src/backend/openvino/excluded_concrete_tests.txt
    • Removed NumpyDtypeTest::test_nanargmax, NumpyDtypeTest::test_nanargmin, NumpyOneInputOpsCorrectnessTest::test_nanargmax, and NumpyOneInputOpsCorrectnessTest::test_nanargmin from the exclusion list.
  • keras/src/backend/openvino/numpy.py
    • Added logic to _resolve_axis to handle OpenVINO Output tensors for axis values, converting them to integers.
    • Integrated similar OpenVINO Output tensor axis resolution into the argmax and argmin functions, ensuring static axis values.
    • Implemented the nanargmax function, handling float64 conversion, replacing NaNs with negative infinity, and delegating to argmax.
    • Implemented the nanargmin function, handling float64 conversion, replacing NaNs with positive infinity, and delegating to argmin.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for nanargmax and nanargmin to the OpenVINO backend, which is a great addition. The implementation correctly handles NaN values by replacing them with infinities. The changes also include enabling the corresponding tests and fixing dynamic axis resolution.

I've identified a few areas for improvement:

  • There's some code duplication in the logic for resolving static axis values, which could be refactored into a helper function.
  • The error message for dynamic axes could be more descriptive, as per the repository's style guide.
  • There's an edge case with an empty axis tuple that behaves differently from NumPy.
  • A small redundant check in both new functions can be removed.

Overall, the changes are good, and with these refinements, the code will be more robust and maintainable.

Note: Security Review did not run due to the size of the PR.

Comment on lines +2907 to +2908
if axis is None:
return OpenVINOKerasTensor(x)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When axis is an empty tuple () or list [], _resolve_axis returns None for the axis. In this case, the function returns the input tensor x unmodified. This behavior is inconsistent with NumPy's nanargmax, which raises a TypeError for an empty tuple axis. Returning the input tensor is likely incorrect as no reduction is performed. Consider raising a TypeError to match NumPy's behavior for this edge case. The same issue exists in the nanargmin implementation.

Comment on lines +343 to +353
if hasattr(axis, "__class__") and "Output" in axis.__class__.__name__:
axis_node = axis.get_node()
if hasattr(axis_node, "get_data"):
axis_val = axis_node.get_data()
if axis_val is not None:
if hasattr(axis_val, "ndim") and axis_val.ndim > 0 and axis_val.size > 0:
axis = int(axis_val[0])
elif hasattr(axis_val, "size") and axis_val.size == 1:
axis = int(axis_val.item())
else:
axis = int(axis_val)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This logic to resolve a static axis value from an OpenVINO tensor is duplicated in argmax (lines 537-551) and argmin (lines 584-598). To improve maintainability and reduce code redundancy, consider extracting this logic into a private helper function. This function could attempt to resolve the axis to an integer and be used in all three places.

Additionally, the type check hasattr(axis, "__class__") and "Output" in axis.__class__.__name__ is fragile. It's better to use isinstance(axis, ov.Output). The argmax and argmin functions use a redundant or condition with both checks. A single, robust isinstance check should be used in the new helper function.

else:
axis = int(axis_val)
else:
raise ValueError("axis must be static for argmax")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the Keras API design guidelines (line 140), error messages should be contextual and actionable. This error message could be improved by providing more context, such as the type of axis received and what is expected. A similar improvement can be applied to the argmin function.

For example:

raise ValueError(
    f"For the OpenVINO backend, `argmax` requires a static `axis` value, "
    f"but received a dynamic tensor. Please provide a static integer "
    f"for the `axis` argument."
)
References
  1. Error messages should be contextual, informative, and actionable, explaining what happened, what was expected, and how the user can fix it. (link)

Comment on lines +2921 to +2922
if result_ov.get_element_type() != Type.i32:
nan_value = ov_opset.convert(nan_value, result_ov.get_element_type())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The argmax function is configured to return indices of type i32. Therefore, result_ov.get_element_type() should always be Type.i32. This conditional check appears to be redundant and can be removed for simplification. A similar redundant check exists in the nanargmin implementation (lines 2956-2957).

@codecov-commenter
Copy link

codecov-commenter commented Mar 8, 2026

Codecov Report

❌ Patch coverage is 52.38095% with 40 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.92%. Comparing base (7598c3e) to head (8988755).

Files with missing lines Patch % Lines
keras/src/backend/openvino/numpy.py 52.38% 25 Missing and 15 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #22383      +/-   ##
==========================================
- Coverage   82.96%   82.92%   -0.04%     
==========================================
  Files         596      596              
  Lines       66269    66353      +84     
  Branches    10321    10348      +27     
==========================================
+ Hits        54982    55026      +44     
- Misses       8667     8692      +25     
- Partials     2620     2635      +15     
Flag Coverage Δ
keras 82.75% <52.38%> (-0.04%) ⬇️
keras-jax 60.75% <0.00%> (-0.08%) ⬇️
keras-numpy 54.96% <0.00%> (-0.07%) ⬇️
keras-openvino 49.06% <52.38%> (+0.01%) ⬆️
keras-tensorflow 61.97% <0.00%> (-0.09%) ⬇️
keras-torch 60.80% <0.00%> (-0.08%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@andersendsa
Copy link
Contributor Author

hi @hertschuh this pr passes all the tests and is ready for review

Comment on lines +343 to +358
if hasattr(axis, "__class__") and "Output" in axis.__class__.__name__:
axis_node = axis.get_node()
if hasattr(axis_node, "get_data"):
axis_val = axis_node.get_data()
if axis_val is not None:
if (
hasattr(axis_val, "ndim")
and axis_val.ndim > 0
and axis_val.size > 0
):
axis = int(axis_val[0])
elif hasattr(axis_val, "size") and axis_val.size == 1:
axis = int(axis_val.item())
else:
axis = int(axis_val)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this added? Tensor axes are not supported.

Comment on lines +541 to +561
if isinstance(axis, ov.Output) or (
hasattr(axis, "__class__") and "Output" in axis.__class__.__name__
):
axis_node = axis.get_node()
if hasattr(axis_node, "get_data"):
axis_val = axis_node.get_data()
if axis_val is not None:
if (
hasattr(axis_val, "ndim")
and axis_val.ndim > 0
and axis_val.size > 0
):
axis = int(axis_val[0])
elif hasattr(axis_val, "size") and axis_val.size == 1:
axis = int(axis_val.item())
else:
axis = int(axis_val)
else:
raise ValueError("axis must be static for argmax")
else:
raise ValueError("axis must be static for argmax")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this added? Tensor axes are not supported.

Comment on lines +594 to +614
if isinstance(axis, ov.Output) or (
hasattr(axis, "__class__") and "Output" in axis.__class__.__name__
):
axis_node = axis.get_node()
if hasattr(axis_node, "get_data"):
axis_val = axis_node.get_data()
if axis_val is not None:
if (
hasattr(axis_val, "ndim")
and axis_val.ndim > 0
and axis_val.size > 0
):
axis = int(axis_val[0])
elif hasattr(axis_val, "size") and axis_val.size == 1:
axis = int(axis_val.item())
else:
axis = int(axis_val)
else:
raise ValueError("axis must be static for argmin")
else:
raise ValueError("axis must be static for argmin")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this added? Tensor axes are not supported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Good First Issue][Keras 3 OpenVINO Backend]: Support nanargmax and nanargmin operations

4 participants