HY-2012's picture
First commit
759d0dd verified
Raw
History Blame Contribute Delete
5.3 kB
#!/usr/bin/env python3
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)))