Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions runtime/onert/api/python/package/common/basesession.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import List
import numpy as np

from ..native import libnnfw_api_pybind
from ..native.libnnfw_api_pybind import infer, tensorinfo


def num_elems(tensor_info):
Expand Down Expand Up @@ -52,6 +53,32 @@ def _recreate_session(self, backend_session):
del self.session # Clean up the existing session
self.session = backend_session

def get_inputs_tensorinfo(self) -> List[tensorinfo]:
"""
Retrieve tensorinfo for all input tensors.

Returns:
list[tensorinfo]: A list of tensorinfo objects for each input.
"""
num_inputs: int = self.session.input_size()
infos: List[tensorinfo] = []
for i in range(num_inputs):
infos.append(self.session.input_tensorinfo(i))
return infos

def get_outputs_tensorinfo(self) -> List[tensorinfo]:
"""
Retrieve tensorinfo for all output tensors.

Returns:
list[tensorinfo]: A list of tensorinfo objects for each output.
"""
num_outputs: int = self.session.output_size()
infos: List[tensorinfo] = []
for i in range(num_outputs):
infos.append(self.session.output_tensorinfo(i))
return infos

def set_inputs(self, size, inputs_array=[]):
"""
Set the input tensors for the session.
Expand Down Expand Up @@ -97,4 +124,4 @@ def set_outputs(self, size):


def tensorinfo():
return libnnfw_api_pybind.infer.nnfw_tensorinfo()
return infer.nnfw_tensorinfo()
99 changes: 75 additions & 24 deletions runtime/onert/api/python/package/infer/session.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,97 @@
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

# TODO: Revise this after discussion to properly support dynamic shapes
# This is a temporary workaround to prevent prepare() errors when tensorinfo dims include -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)
Copy link
Copy Markdown
Contributor

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?

Copy link
Copy Markdown
Contributor Author

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 ‑1 to 1. I just ran it with onert_run and saw the same failure, so this looks like a bug in onert. We need to fix this bug and update this code to allow ‑1 in inputs. I'll try it.

Copy link
Copy Markdown
Contributor

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 .

Copy link
Copy Markdown
Contributor Author

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:

  1. 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.

  2. Dynamic shape modification
    If user's inputs change shape on each run, users must call update_inputs_tensorinfo(new_infos) before calling prepare(). If users skip that and any tensorinfo.dims still contains -1, users will hit the same shape error at runtime. So how should users specify dims values that change each time? If they set any of them to -1, an error will occur.

Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

how should users specify dims values that change each time? If they set any of them to -1, an error will occur.

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.


def compile(self, path: str, backends: str = "cpu"):
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,
or if any tensorinfo contains a negative dimension.
"""
# 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):
# Check for any negative dimension in the specified rank
if any(d < 0 for d in info.dims[:info.rank]):
raise ValueError(
f"Input tensorinfo at index {i} contains negative dimension(s): "
f"{info.dims[:info.rank]}")
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
14 changes: 11 additions & 3 deletions runtime/onert/sample/minimal-python/src/minimal.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from onert import infer
import numpy as np
import sys


Expand All @@ -8,10 +9,17 @@ def main(nnpackage_path, backends="cpu"):
session = infer.session(nnpackage_path, backends)

# Prepare input. Here we just allocate dummy input arrays.
input_size = session.input_size()
session.set_inputs(input_size)
input_infos = session.get_inputs_tensorinfo()
dummy_inputs = []
for info in input_infos:
# Retrieve the dimensions list from tensorinfo property.
dims = list(info.dims)
# Build the shape tuple from tensorinfo dimensions.
shape = tuple(dims[:info.rank])
# Create a dummy numpy array filled with zeros.
dummy_inputs.append(np.zeros(shape, dtype=info.dtype))

outputs = session.inference()
outputs = session.infer(dummy_inputs)

print(f"nnpackage {nnpackage_path.split('/')[-1]} runs successfully.")
return
Expand Down
52 changes: 52 additions & 0 deletions runtime/onert/sample/minimal-python/src/static_shape_inference.py
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)