| |
| """axengine / onnxruntime 推理会话封装。""" |
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
| from pathlib import Path |
| from typing import Any |
|
|
| import numpy as np |
|
|
|
|
| @dataclass(frozen=True) |
| class TensorInfo: |
| name: str |
| shape: tuple[int, ...] |
| dtype: np.dtype |
|
|
|
|
| def _numpy_dtype(value: Any) -> np.dtype: |
| text = str(value).lower() |
| mapping = ( |
| (("tensor(float)", "float32", "fp32", "f32"), np.float32), |
| (("tensor(float16)", "float16", "fp16", "f16"), np.float16), |
| (("tensor(int64)", "int64", "s64"), np.int64), |
| (("tensor(int32)", "int32", "s32"), np.int32), |
| (("tensor(uint16)", "uint16", "u16"), np.uint16), |
| (("tensor(uint8)", "uint8", "u8"), np.uint8), |
| ) |
| for aliases, dtype in mapping: |
| if any(alias in text for alias in aliases): |
| return np.dtype(dtype) |
| raise ValueError(f"unsupported runtime tensor dtype: {value}") |
|
|
|
|
| def _tensor_info(value: Any) -> TensorInfo: |
| shape = getattr(value, "shape", None) |
| if shape is None: |
| shape = getattr(value, "dims", None) |
| if shape is None or any(dim is None for dim in shape): |
| raise ValueError(f"dynamic or missing tensor shape for {value.name}: {shape}") |
| dtype = getattr(value, "dtype", None) |
| if dtype is None: |
| dtype = getattr(value, "type", None) |
| return TensorInfo( |
| name=value.name, |
| shape=tuple(int(dim) for dim in shape), |
| dtype=_numpy_dtype(dtype), |
| ) |
|
|
|
|
| class InferenceSession: |
| """.axmodel -> axengine;.onnx -> onnxruntime CPU。""" |
|
|
| def __init__(self, model_path: str | Path, backend: str | None = None): |
| self.path = Path(model_path) |
| if not self.path.is_file(): |
| raise FileNotFoundError(self.path) |
| if backend is None: |
| backend = "axengine" if self.path.suffix == ".axmodel" else "onnx" |
| self.backend = backend |
| if backend == "axengine": |
| try: |
| import axengine |
| except ImportError as error: |
| raise RuntimeError( |
| "axengine is unavailable; run this backend on an AXERA board" |
| ) from error |
| self._session = axengine.InferenceSession(str(self.path)) |
| elif backend == "onnx": |
| try: |
| import onnxruntime as ort |
| except ImportError as error: |
| raise RuntimeError("onnxruntime is required for --backend onnx") from error |
| options = ort.SessionOptions() |
| options.inter_op_num_threads = 1 |
| options.intra_op_num_threads = 1 |
| self._session = ort.InferenceSession( |
| str(self.path), |
| sess_options=options, |
| providers=["CPUExecutionProvider"], |
| ) |
| else: |
| raise ValueError(f"unsupported backend: {backend}") |
|
|
| self.inputs = [_tensor_info(value) for value in self._session.get_inputs()] |
| self.outputs = [_tensor_info(value) for value in self._session.get_outputs()] |
| self.input_by_name = {value.name: value for value in self.inputs} |
|
|
| def run(self, feed: dict[str, np.ndarray]) -> dict[str, np.ndarray]: |
| missing = [value.name for value in self.inputs if value.name not in feed] |
| if missing: |
| raise KeyError(f"missing inputs for {self.path.name}: {missing}") |
| prepared = { |
| name: np.ascontiguousarray(np.asarray(feed[name], dtype=meta.dtype)) |
| for name, meta in self.input_by_name.items() |
| } |
| values = self._session.run(None, prepared) |
| if isinstance(values, dict): |
| return {name: np.asarray(value) for name, value in values.items()} |
| if not isinstance(values, (list, tuple)): |
| values = [values] |
| if len(values) != len(self.outputs): |
| raise RuntimeError( |
| f"unexpected output count from {self.path.name}: " |
| f"{len(values)} != {len(self.outputs)}" |
| ) |
| return { |
| meta.name: np.asarray(value) |
| for meta, value in zip(self.outputs, values) |
| } |
|
|
|
|
| def first_output(session: InferenceSession, outputs: dict[str, np.ndarray]) -> np.ndarray: |
| return outputs[session.outputs[0].name] |
|
|