Skip to content

[OpenVINO backend] Support for LSTM #22313

Closed
andersendsa wants to merge 16 commits intokeras-team:masterfrom
andersendsa:openvino-lstm
Closed

[OpenVINO backend] Support for LSTM #22313
andersendsa wants to merge 16 commits intokeras-team:masterfrom
andersendsa:openvino-lstm

Conversation

@andersendsa
Copy link
Contributor

Details :
Implement LSTM operation for the OpenVINO backend.
Closes issue : openvinotoolkit/openvino#34403

@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 by introducing full support for the Long Short-Term Memory (LSTM) operation. This integration allows Keras models leveraging LSTMs to be efficiently compiled and executed using OpenVINO, addressing a critical gap in functionality. The changes involve a detailed implementation of the lstm function using OpenVINO's opset, alongside necessary adjustments to tensor processing utilities and test configurations to ensure compatibility and correct behavior.

Highlights

  • LSTM Operation Support: Implemented comprehensive support for the Long Short-Term Memory (LSTM) recurrent neural network operation within the OpenVINO backend, replacing a NotImplementedError.
  • Tensor Shape Handling: Refined tensor shape manipulation within the rnn.py module, including adjustments to ov_opset.constant calls, new_shape calculations, and the addition of squeeze and unsqueeze operations to correctly manage tensor dimensions for OpenVINO.
  • Utility Function Updates: Updated the cudnn_ok utility function to reflect new parameters and logic for evaluating CuDNN compatibility, and added gru_test.py to the excluded tests list.

🧠 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_tests.txt
    • Added keras/src/layers/rnn/gru_test.py to the list of excluded tests.
  • keras/src/backend/openvino/rnn.py
    • Imported backend, split, and concatenate from Keras's OpenVINO numpy backend.
    • Modified the _slice_at_0 helper function to correctly handle tensor shapes by adjusting ov_opset.constant calls and new_shape calculations.
    • Introduced squeeze operations for sliced inputs and masks, and unsqueeze for final outputs, to manage tensor dimensions.
    • Implemented the lstm function, converting Keras LSTM parameters into OpenVINO lstm_sequence operations, including handling initial states, kernels, biases, activations, sequence direction, and output formatting.
    • Updated the cudnn_ok function signature and logic to properly evaluate CuDNN compatibility based on activation functions and unrolling status.
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 the LSTM operation to the OpenVINO backend by implementing the backend.lstm function, leveraging the LSTMSequence operation from OpenVINO. The changes look good overall, but I have a few suggestions to improve code quality, robustness, and clarity. My feedback includes removing some dead and redundant code, improving error handling for unsupported activations, and clarifying the purpose of some changes. I've also noted a potentially out-of-scope change in the test exclusion list.

Comment on lines +350 to +358
direction = "forward"
if go_backwards:
direction = "reverse"
def get_activation_name(act):
from keras.src import activations
if act == activations.tanh: return "tanh"
if act == activations.sigmoid: return "sigmoid"
if act == activations.relu: return "relu"
if act == activations.linear: return "linear"
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The get_activation_name helper function defaults to returning "tanh" for any unrecognized activation function. This can lead to silent errors where an unsupported activation is used but the model compiles and runs with a different activation (tanh) than intended. According to the Keras API design guidelines (lines 139-143), it's better to catch user errors early and provide informative error messages.

Please consider raising a ValueError for unsupported activations. You could also expand the list of recognized activations to include those from keras.src.ops (e.g., ops.tanh) for better robustness.

    def get_activation_name(act):
        from keras.src import activations
        from keras.src import ops
        if act in (activations.tanh, ops.tanh):
            return "tanh"
        if act in (activations.sigmoid, ops.sigmoid):
            return "sigmoid"
        if act in (activations.relu, ops.relu):
            return "relu"
        if act in (activations.linear, ops.linear):
            return "linear"
        raise ValueError(f"Unsupported activation function for OpenVINO LSTM: {act}")

keras/src/trainers/trainer_test.py
keras/src/utils No newline at end of file
keras/src/utils
keras/src/layers/rnn/gru_test.py No newline at end of file
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 change to exclude gru_test.py seems out of scope for a pull request focused on adding LSTM support. Additionally, the entire keras/src/layers/rnn directory is already excluded on line 26, which makes this new line redundant if the directory exclusion works as expected. Could you please clarify the reasoning for this change or remove it if it's not necessary?

recurrent_kernel,
bias,
activation,
recurrent_activation,
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 unroll parameter is included in the function signature but is not used within the implementation. According to the Keras API design guidelines (lines 18 and 53), unused parameters should be avoided to keep the API surface clean and minimize user confusion. Please consider either implementing the unrolling logic or removing this parameter.

Comment on lines +307 to +308
unroll=False,
time_major=False,
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

These local imports are redundant. keras.src.backend is already imported at the top of the file, and split and concatenate are also imported from keras.src.backend.openvino.numpy at the file's top level. These lines can be removed to improve code clarity.

Comment on lines +408 to +425
)


def cudnn_ok(*args, **kwargs):
return False
def cudnn_ok(
activation,
recurrent_activation,
unroll,
use_bias,
reset_after=None,
):
if reset_after is None:
return (
activation in (activations.tanh, ops.tanh)
and recurrent_activation in (activations.sigmoid, ops.sigmoid)
and not unroll
)
else:
return False
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 cudnn_ok function appears to be dead code in the context of the OpenVINO backend. It's only referenced in the LSTM and GRU layer __init__ methods within a condition that checks backend.backend() == "tensorflow". When the backend is OpenVINO, this function will not be called.

Additionally, the name cudnn_ok is misleading here. If this function is intended to be used in the future for OpenVINO, it should be renamed to something more appropriate, like is_fast_path_supported.

For now, it seems this function can be reverted to return False to avoid confusion and maintaining unused code.

@codecov-commenter
Copy link

codecov-commenter commented Feb 28, 2026

Codecov Report

❌ Patch coverage is 2.19780% with 89 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.54%. Comparing base (7598c3e) to head (486ecb6).

Files with missing lines Patch % Lines
keras/src/backend/openvino/rnn.py 2.40% 81 Missing ⚠️
keras/src/layers/rnn/gru.py 0.00% 8 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (7598c3e) and HEAD (486ecb6). Click for more details.

HEAD has 2 uploads less than BASE
Flag BASE (7598c3e) HEAD (486ecb6)
keras 5 4
keras-openvino 1 0
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #22313      +/-   ##
==========================================
- Coverage   82.96%   76.54%   -6.43%     
==========================================
  Files         596      596              
  Lines       66269    66355      +86     
  Branches    10321    10335      +14     
==========================================
- Hits        54982    50790    -4192     
- Misses       8667    13140    +4473     
+ Partials     2620     2425     -195     
Flag Coverage Δ
keras 76.37% <2.19%> (-6.42%) ⬇️
keras-jax 60.75% <2.19%> (-0.08%) ⬇️
keras-numpy 54.96% <0.00%> (-0.08%) ⬇️
keras-openvino ?
keras-tensorflow 61.98% <2.19%> (-0.08%) ⬇️
keras-torch 60.80% <2.19%> (-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 it is ready for review

Copy link
Contributor

@goyaladitya05 goyaladitya05 left a comment

Choose a reason for hiding this comment

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

I've left some comments. Please address the gemini-generated comments as well. They seem to be valid.

keras/src/trainers/trainer_test.py
keras/src/utils No newline at end of file
keras/src/utils
keras/src/layers/rnn/gru_test.py No newline at end of file
Copy link
Contributor

@goyaladitya05 goyaladitya05 Mar 1, 2026

Choose a reason for hiding this comment

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

Agreed with gemini here. Adding gru_test.py when the parent directory is already excluded makes no sense. Please clean up the excluded_tests.txt diff.

Copy link
Contributor

Choose a reason for hiding this comment

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

Manually overriding dir and getattr to force-import the entire library seems unnecessary. We only make changes to the files involving the backend, for a backend specific implementation. Is there a reason for this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1

Revert this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Revert this whole file, it's failing the code format check.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm confused by the inclusion of these changes in this PR. keras/src/initializers/ is a global utility and should remain backend-agnostic. Unless there is a specific reason these initializers need to be changed for all of Keras, I'd suggest reverting the changes to this file.

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1

Why was this change needed?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Revert this whole file, it's failing the code format check.

Copy link
Collaborator

@hertschuh hertschuh left a comment

Choose a reason for hiding this comment

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

Thanks for implementing this!

Comment on lines +479 to +485
def cudnn_ok(
activation,
recurrent_activation,
unroll,
use_bias,
reset_after=None,
):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just to clarify, in this context, cudnn_ok means "use the native OpenVino implementation" (we should rename it).

But is the criteria really the same as for CuDNN? That seems surprising.

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1

Why was this change needed?

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1

Revert this.

from openvino import Type

from keras.src import activations
from keras.src import ops
Copy link
Collaborator

Choose a reason for hiding this comment

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

You cannot import ops like this here, this is what's creating the circular imports.

But they're not even used.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Revert this whole file, it's failing the code format check.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Revert this whole file, it's failing the code format check.

Comment on lines +479 to +480
def cudnn_ok(*args , **kwargs):
return False No newline at end of file
Copy link
Collaborator

Choose a reason for hiding this comment

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

Revert this change file, it's failing the code format check.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey @hertschuh just wanted to double-check—do you want me to revert the whole file back to the previous commit?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, no, just the last 2 lines of this file.

direction = "reverse"

def get_activation_name(act):
from keras.src import activations
Copy link
Collaborator

Choose a reason for hiding this comment

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

activations are already imported at the top, you don't need this import.

@goyaladitya05
Copy link
Contributor

goyaladitya05 commented Mar 5, 2026

Hey @andersendsa, could you also enable the tests releted to LSTM from excluded_concrete_tests.txt ?

@andersendsa
Copy link
Contributor Author

hi @hertschuh i have reverted the file and removed the test cases the pr is ready for review

@andersendsa
Copy link
Contributor Author

/gemini review

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 the LSTM operation in the OpenVINO backend by implementing the lstm function using the ov_opset.lstm_sequence operation. This is a great addition. The changes also include modifications to the generic rnn function and enabling LSTM tests.

My review found a critical issue with the gate ordering for weights and biases in the new lstm implementation, which will lead to incorrect results. I've also identified a few other issues, including duplicated code, unreachable code, and dead code, which should be addressed to improve code quality and correctness.

k_i, k_f, k_c, k_o = ov_opset.split(
kernel_T, ov_opset.constant(0, Type.i32), 4
).outputs()
W = ov_opset.concat([k_f, k_i, k_c, k_o], axis=0).output(0)
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The concatenation order for weights W is incorrect. Keras LSTM kernels are ordered for [input, forget, cell, output] gates. The OpenVINO LSTMSequence operation expects the weights for gates in the order [input, output, forget, cell]. The current code concatenates them in the order [forget, input, cell, output]. This should be [input, output, forget, cell].

Suggested change
W = ov_opset.concat([k_f, k_i, k_c, k_o], axis=0).output(0)
W = ov_opset.concat([k_i, k_o, k_f, k_c], axis=0).output(0)

r_i, r_f, r_c, r_o = ov_opset.split(
recurrent_kernel_T, ov_opset.constant(0, Type.i32), 4
).outputs()
R = ov_opset.concat([r_f, r_i, r_c, r_o], axis=0).output(0)
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

Similar to the kernel weights W, the concatenation order for recurrent weights R is incorrect. It should be [input, output, forget, cell] to match the OpenVINO LSTMSequence op specification.

Suggested change
R = ov_opset.concat([r_f, r_i, r_c, r_o], axis=0).output(0)
R = ov_opset.concat([r_i, r_o, r_f, r_c], axis=0).output(0)

b_i, b_f, b_c, b_o = ov_opset.split(
bias, ov_opset.constant(0, Type.i32), 4
).outputs()
B = ov_opset.concat([b_f, b_i, b_c, b_o], axis=0).output(0)
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

Similar to the weights, the concatenation order for biases B is incorrect. It should be [input, output, forget, cell] to match the OpenVINO LSTMSequence op specification.

Suggested change
B = ov_opset.concat([b_f, b_i, b_c, b_o], axis=0).output(0)
B = ov_opset.concat([b_i, b_o, b_f, b_c], axis=0).output(0)

Comment on lines +241 to +244
zero_const = ov_opset.constant([0], dtype=Type.i32).output(0)
final_output_list = [
ov_opset.unsqueeze(x, zero_const).output(0) for x in final_output_list
]
Copy link
Contributor

Choose a reason for hiding this comment

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

high

This block of code, which redefines zero_const and re-assigns final_output_list by unsqueezing its elements, is a duplicate of the block on lines 236-239. This is redundant and should be removed to avoid confusion and potential bugs.

recurrent_activation,
return_sequences=False,
go_backwards=False,
unroll=False,
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 unroll parameter is defined but not used in this function. According to the Keras API design guidelines, parameters that are not used should be removed to avoid user confusion and to keep the API surface clean.

):
if mask is not None:
raise NotImplementedError("lstm sequence with mask is not supported")
mask = get_ov_output(mask)
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 line is unreachable due to the raise NotImplementedError on the preceding line. It should be removed.

Comment on lines +396 to +408
def get_activation_name(act):

if act == activations.tanh:
return "tanh"
if act == activations.sigmoid:
return "sigmoid"
if act == activations.relu:
return "relu"
if act == activations.linear:
return "linear"
if hasattr(act, "__name__"):
return act.__name__
return "tanh"
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 function can be made more concise and maintainable by using a dictionary to map activation functions to their string names. This is a common pattern for this type of logic and would improve readability.

For example:

ACTIVATION_MAP = {
    activations.tanh: "tanh",
    activations.sigmoid: "sigmoid",
    activations.relu: "relu",
    activations.linear: "linear",
}

def get_activation_name(act):
    name = ACTIVATION_MAP.get(act)
    if name:
        return name
    if hasattr(act, "__name__"):
        return act.__name__
    return "tanh"

Comment on lines +444 to +452
if not return_sequences:
if time_major:
outputs = ov_opset.unsqueeze(
Ho, ov_opset.constant([0], Type.i32)
).output(0)
else:
outputs = ov_opset.unsqueeze(
Ho, ov_opset.constant([1], Type.i32)
).output(0)
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

When return_sequences is False, the caller (keras.layers.RNN) uses the last_output return value and ignores the outputs value. This block of code modifies outputs when return_sequences=False, which makes it dead code. This block should be removed to improve clarity and avoid confusion.

@goyaladitya05
Copy link
Contributor

goyaladitya05 commented Mar 9, 2026 via email

@andersendsa andersendsa closed this Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants