| |
| from __future__ import annotations |
|
|
| import json |
| import wave |
| 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(int16)", "int16", "s16"), np.int16), |
| (("tensor(int8)", "int8", "s8"), np.int8), |
| (("tensor(uint16)", "uint16", "u16"), np.uint16), |
| (("tensor(uint8)", "uint8", "u8"), np.uint8), |
| (("tensor(bool)", "bool"), np.bool_), |
| ) |
| 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) |
| 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: |
| """Small common wrapper for ONNX Runtime and AXEngine.""" |
|
|
| def __init__(self, model_path: str | Path, backend: str): |
| self.path = Path(model_path) |
| if not self.path.is_file(): |
| raise FileNotFoundError(self.path) |
| 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] |
|
|
|
|
| def read_wav(path: str | Path) -> tuple[np.ndarray, int]: |
| path = Path(path) |
| with wave.open(str(path), "rb") as source: |
| if source.getnchannels() != 1 or source.getsampwidth() != 2: |
| raise ValueError(f"expected mono 16-bit PCM WAV: {path}") |
| sample_rate = source.getframerate() |
| samples = np.frombuffer( |
| source.readframes(source.getnframes()), dtype=np.int16 |
| ).copy() |
| return samples, sample_rate |
|
|
|
|
| def load_json(path: str | Path) -> dict[str, Any]: |
| return json.loads(Path(path).read_text()) |
|
|
|
|
| def write_json(path: str | Path, value: Any) -> None: |
| path = Path(path) |
| path.parent.mkdir(parents=True, exist_ok=True) |
| path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n") |
|
|
|
|
| def max_abs_delta(reference: Any, candidate: Any) -> float: |
| reference_array = np.asarray(reference, dtype=np.float64) |
| candidate_array = np.asarray(candidate, dtype=np.float64) |
| if reference_array.shape != candidate_array.shape: |
| return float("inf") |
| if reference_array.size == 0: |
| return 0.0 |
| return float(np.max(np.abs(reference_array - candidate_array))) |
|
|