-
Notifications
You must be signed in to change notification settings - Fork 179
[onert/python] Support static shape modification across inference API #15184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
hseok-oh
merged 4 commits into
Samsung:master
from
ragmani:onert/python/support_static_shape_inference
Apr 21, 2025
Merged
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,46 +1,90 @@ | ||
| from ..native import libnnfw_api_pybind | ||
| from typing import List, Any | ||
| import numpy as np | ||
|
|
||
| from ..native.libnnfw_api_pybind import infer, tensorinfo | ||
| from ..common.basesession import BaseSession | ||
|
|
||
|
|
||
| class session(BaseSession): | ||
| """ | ||
| Class for inference using nnfw_session. | ||
| """ | ||
| def __init__(self, path: str = None, backends: str = "cpu"): | ||
| def __init__(self, path: str, backends: str = "cpu") -> None: | ||
| """ | ||
| Initialize the inference session. | ||
|
|
||
| Args: | ||
| path (str): Path to the model file or nnpackage directory. | ||
| backends (str): Backends to use, default is "cpu". | ||
| """ | ||
| if path is not None: | ||
| super().__init__(libnnfw_api_pybind.infer.nnfw_session(path, backends)) | ||
| self.session.prepare() | ||
| self.set_outputs(self.session.output_size()) | ||
| else: | ||
| super().__init__() | ||
| super().__init__(infer.nnfw_session(path, backends)) | ||
| self._prepared: bool = False | ||
|
|
||
| def compile(self, path: str, backends: str = "cpu"): | ||
| # Replace any dynamic dimension (-1) with 1 before static shape inference, | ||
| # because nnfw_session cannot perform static shape inference on input dimensions set to -1. | ||
| original_infos: List[tensorinfo] = self.get_inputs_tensorinfo() | ||
| fixed_infos: List[tensorinfo] = [] | ||
| for info in original_infos: | ||
| dims = list(info.dims) | ||
| # replace -1 with 1 | ||
| dims = [1 if d == -1 else d for d in dims] | ||
| info.dims = dims # assume setter accepts a list | ||
| fixed_infos.append(info) | ||
| # update tensorinfo in session | ||
| self.update_inputs_tensorinfo(fixed_infos) | ||
|
|
||
| def update_inputs_tensorinfo(self, new_infos: List[tensorinfo]) -> None: | ||
| """ | ||
| Prepare the session by recreating it with new parameters. | ||
| Update all input tensors' tensorinfo at once. | ||
|
|
||
| Args: | ||
| path (str): Path to the model file or nnpackage directory. Defaults to the existing path. | ||
| backends (str): Backends to use. Defaults to the existing backends. | ||
| new_infos (list[tensorinfo]): A list of updated tensorinfo objects for the inputs. | ||
|
|
||
| Raises: | ||
| ValueError: If the number of new_infos does not match the session's input size. | ||
| """ | ||
| # Update parameters if provided | ||
| if path is None: | ||
| raise ValueError("path must not be None.") | ||
| # Recreate the session with updated parameters | ||
| self._recreate_session(libnnfw_api_pybind.infer.nnfw_session(path, backends)) | ||
| # Prepare the new session | ||
| self.session.prepare() | ||
| self.set_outputs(self.session.output_size()) | ||
|
|
||
| def inference(self): | ||
| num_inputs: int = self.session.input_size() | ||
| if len(new_infos) != num_inputs: | ||
| raise ValueError( | ||
| f"Expected {num_inputs} input tensorinfo(s), but got {len(new_infos)}.") | ||
| for i, info in enumerate(new_infos): | ||
| self.session.set_input_tensorinfo(i, info) | ||
|
|
||
| def infer(self, inputs_array: List[np.ndarray]) -> List[np.ndarray]: | ||
| """ | ||
| Perform model and get outputs | ||
| Run a complete inference cycle: | ||
| - If the session has not been prepared or outputs have not been set, call prepare() and set_outputs(). | ||
| - Automatically configure input buffers based on the provided numpy arrays. | ||
| - Execute the inference session. | ||
| - Return the output tensors with proper multi-dimensional shapes. | ||
|
|
||
| This method supports both static and dynamic shape modification: | ||
| - If update_inputs_tensorinfo() has been called before running inference, the model is compiled | ||
| with the fixed static input shape. | ||
| - Otherwise, the input shapes can be adjusted dynamically. | ||
|
|
||
| Args: | ||
| inputs_array (list[np.ndarray]): List of numpy arrays representing the input data. | ||
|
|
||
| Returns: | ||
| list: Outputs from the model. | ||
| list[np.ndarray]: A list containing the output numpy arrays. | ||
| """ | ||
| # Check if the session is prepared. If not, call prepare() and set_outputs() once. | ||
| if not self._prepared: | ||
| self.session.prepare() | ||
| self.set_outputs(self.session.output_size()) | ||
| self._prepared = True | ||
|
|
||
| # Verify that the number of provided inputs matches the session's expected input count. | ||
| expected_input_size: int = self.session.input_size() | ||
| if len(inputs_array) != expected_input_size: | ||
| raise ValueError( | ||
| f"Expected {expected_input_size} input(s), but received {len(inputs_array)}." | ||
| ) | ||
|
|
||
| # Configure input buffers using the current session's input size and provided data. | ||
| self.set_inputs(expected_input_size, inputs_array) | ||
| # Execute the inference. | ||
| self.session.run() | ||
| # Return the output buffers. | ||
| return self.outputs | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
runtime/onert/sample/minimal-python/src/static_shape_inference.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| from onert import infer | ||
| import numpy as np | ||
| import sys | ||
|
|
||
|
|
||
| def main(nnpackage_path, backends="cpu"): | ||
| # Create session and load the nnpackage | ||
| sess = infer.session(nnpackage_path, backends) | ||
|
|
||
| # Retrieve the current tensorinfo for all inputs. | ||
| current_input_infos = sess.get_inputs_tensorinfo() | ||
|
|
||
| # Create new tensorinfo objects with a static shape modification. | ||
| # For this example, assume we change the first dimension (e.g., batch size) to 10. | ||
| new_input_infos = [] | ||
| for info in current_input_infos: | ||
| # For example, if the current shape is (?, 4), update it to (10, 4). | ||
| # We copy the current info and modify the rank and dims. | ||
| # (Note: Depending on your model, you may want to modify additional dimensions.) | ||
| new_shape = [10] + list(info.dims[1:info.rank]) | ||
| info.rank = len(new_shape) | ||
| for i, dim in enumerate(new_shape): | ||
| info.dims[i] = dim | ||
| # For any remaining dimensions up to NNFW_MAX_RANK, set them to a default (1). | ||
| for i in range(len(new_shape), len(info.dims)): | ||
| info.dims[i] = 1 | ||
| new_input_infos.append(info) | ||
|
|
||
| # Update all input tensorinfos in the session at once. | ||
| # This will call prepare() and set_outputs() internally. | ||
| sess.update_inputs_tensorinfo(new_input_infos) | ||
|
|
||
| # Create dummy input arrays based on the new static shapes. | ||
| dummy_inputs = [] | ||
| for info in new_input_infos: | ||
| # Build the shape tuple from tensorinfo dimensions. | ||
| shape = tuple(info.dims[:info.rank]) | ||
| # Create a dummy numpy array filled with zeros. | ||
| dummy_inputs.append(np.zeros(shape, dtype=info.dtype)) | ||
|
|
||
| # Run inference with the new static input shapes. | ||
| outputs = sess.infer(dummy_inputs) | ||
|
|
||
| print( | ||
| f"Static shape modification sample: nnpackage {nnpackage_path.split('/')[-1]} runs successfully." | ||
| ) | ||
| return | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| argv = sys.argv[1:] | ||
| main(*argv) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(just curious)
If dynamic dimension (-1) is changed to 1 before shape interference,
I think we do not know whether dynamic shape is included in session.
In this case, how should we deal with dynamic shape?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your advice. If an input contains
‑1, it still errors, so I only applied a fragmentary fix by forcing‑1to1. I just ran it withonert_runand saw the same failure, so this looks like a bug inonert. We need to fix this bug and update this code to allow‑1in inputs. I'll try it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(In my opinion)
It seems that the user did in a wrong scenario rather than a bug. and If the user does not know this changes(-1 -> 1), it will be difficult to debug in case of dynamic shape.
so, (optional) it would be better to print an error message that the shape should be set explicitly before runnig the unknown shape .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are really two modes here:
Static shape modification
Users call
update_inputs_tensorinfo()once (e.g. at construction) with a fully‑specified shape (no –1s), and the session is prepared with that fixed shape.Dynamic shape modification
If user's inputs change shape on each run, users must call
update_inputs_tensorinfo(new_infos)before callingprepare(). If users skip that and anytensorinfo.dimsstill contains-1, users will hit the same shape error at runtime. So how should users specifydimsvalues that change each time? If they set any of them to-1, an error will occur.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed offline, I’ll keep this code unchanged and add a TODO comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that we can't deal with all scenarios.
but it means to maintain the error caused by unknown shape rather than changing to
-1 -> 1.and, if
-1 -> 1, it can be misunderstood that there is an unknown shape and it work without any problems, so i hope it is to solve this.