feat(actpred): 新增 Hailo HEF 推理封装 HailoPredictor,附 AI HAT 2 调研与测速报告

零 torch 依赖(hailo_platform + numpy + PIL),vstream 形状/量化参数从
HEF 动态推导,支持 uint8 量化边缘与动作归一化 stats;新增 6 个免硬件
单测(fake hailo_platform 注入)。调研报告记录 hailort 5.x 新 API 踩坑,
测速报告给出 CPU(ORT 5.9ms) vs NPU(resnet50 4.29ms) 基准与部署建议。
This commit is contained in:
qing committed 2026-08-13 01:41:42 +08:00
1 parent 8673515d31
commit ab60b3cafb
5 files changed
+685

No files matched your search

+30
View File
@@ -152,3 +152,33 @@ print(predictor.action_dim, predictor.history_len, predictor.frame_len) # 公
action = predictor.predict(images, action_history) # np.ndarray (action_dim,) action = predictor.predict(images, action_history) # np.ndarray (action_dim,)
# action_history 形状须为 (history_len, action_dim) # action_history 形状须为 (history_len, action_dim)
``` ```
## Hailo 部署(AI HAT 2 / Hailo-10H)
树莓派 AI HAT 2 上通过 `hailo_infer.py` 的 `HailoPredictor` 走 NPU 推理,
接口与 `ActionPredictorInference` 对齐。模型需先经 Hailo DFC 编译为 HEF:
```bash
# 1. 导出 ONNX
bash _run.sh export --model checkpoints/best_inference.pt --output model.onnx
# 2. 在 x86 主机(或 Hailo 云端 DFC)上编译 + 量化校准 → model.hef,拷回树莓派
# 3. 推理(零 torch 依赖,仅需 hailo_platform + numpy + PIL)
```
```python
from hailo_infer import HailoPredictor
with HailoPredictor("model.hef") as predictor: # stats_path= 仅归一化模型需要
action = predictor.predict(images, action_history)
```
```bash
python3 hailo_infer.py --hef model.hef --images f1.jpg f2.jpg f3.jpg \
--action-history "45,0,...:45,0,..." [--stats action_stats.npy]
```
HEF 的 vstream 名称/形状/量化参数全部从 `HEF.get_input/output_vstream_infos()`
动态推导,不硬编码;图像输入按 Hailo 惯例为 uint8 原始像素
(ImageNet 归一化由 DFC 编译时折叠进网络)。
环境调研与 API 踩坑记录见
[docs/reports/actpred_hailo_hat2_inference_research_20260813.md](../docs/reports/actpred_hailo_hat2_inference_research_20260813.md)。
+250
View File
@@ -0,0 +1,250 @@
"""actpred HEF(Hailo)推理封装 —— AI HAT 2 / Hailo-10H 部署后端
接口对齐 predict.ActionPredictorInference:predict(images, action_history)。
HEF 由 actpred ONNX 经 Hailo DFC 编译生成(量化校准后)。本封装不硬编码
vstream 名称/形状/量化参数,全部从 HEF 的 input/output vstream info 动态推导:
图像输入: (1, F, 3, S, S) → NHWC uint8 (1, F, S, S, 3)
或 (1, S, S, F*3) → 帧沿通道拼接
动作输入: (1, H, D) float32,uint8 边缘时按 quant_info 量化
动作输出: (1, D),uint8 边缘时按 quant_info 反量化
背景与踩坑记录见
docs/reports/actpred_hailo_hat2_inference_research_20260813.md:
- hailort 5.x 对 Hailo-10 仅支持新 API(create_infer_model),
旧版 VDevice.configure(hef) 报 HAILO_NOT_IMPLEMENTED;
- 绑定缓冲区 dtype/大小必须匹配 HEF 边缘格式(量化 uint8);
- 图像输入按 Hailo 惯例为 uint8 原始像素(ImageNet 归一化由
DFC 编译时折叠进网络)。
"""
import argparse
import contextlib
import json
import os
import numpy as np
from PIL import Image
# 与 predict.parse_action_history 相同逻辑;此处内联以保持
# 本模块零 torch 依赖(HAT 部署环境只装 hailo_platform + numpy + PIL)
def parse_action_history(s: str, action_dim: int) -> np.ndarray:
steps = s.split(":")
history = []
for step in steps:
values = [float(v) for v in step.split(",")]
if len(values) != action_dim:
raise ValueError(
f"Expected {action_dim} values per step, got {len(values)}")
history.append(values)
return np.array(history, dtype=np.float32)
class HailoPredictor:
"""HEF 动作预测推理器(Hailo NPU 后端)
用法:
with HailoPredictor("actpred.hef") as predictor:
action = predictor.predict(images, action_history)
"""
def __init__(self, hef_path: str, stats_path: str = None, vdevice=None):
from hailo_platform import HEF, VDevice
self._stack = contextlib.ExitStack()
if vdevice is None:
vdevice = self._stack.enter_context(VDevice())
self._vdevice = vdevice
self._infer_model = self._vdevice.create_infer_model(str(hef_path))
self._configured = self._stack.enter_context(
self._infer_model.configure())
try:
hef = HEF(str(hef_path))
self._in_infos = hef.get_input_vstream_infos()
self._out_infos = hef.get_output_vstream_infos()
if len(self._out_infos) != 1:
raise ValueError(
f"HEF 应只有一个输出 vstream,实际 {len(self._out_infos)}")
self._image_info = self._find_image_input()
self._history_info = self._find_history_input()
self._out_info = self._out_infos[0]
self._derive_dims()
self._action_mean = None
self._action_std = None
if stats_path is not None:
if not os.path.exists(stats_path):
raise FileNotFoundError(f"统计文件不存在: {stats_path}")
stats = np.load(stats_path)
if stats.shape != (2, self.action_dim):
raise ValueError(
f"action_stats.npy 形状应为 (2, {self.action_dim}),"
f"实际为 {stats.shape}: {stats_path}")
self._action_mean = stats[0].astype(np.float32)
self._action_std = stats[1].astype(np.float32)
except Exception:
self._stack.close() # 初始化失败也释放设备,避免 fd 泄漏
raise
# ---------------- 形状推导 ----------------
def _find_image_input(self):
cands = [i for i in self._in_infos if len(i.shape) in (4, 5)]
if len(cands) != 1:
raise ValueError(
"HEF 输入中无法唯一识别图像 vstream(期望一个 4/5 维输入),"
f"实际: {[(i.name, tuple(i.shape)) for i in self._in_infos]}")
return cands[0]
def _find_history_input(self):
cands = [i for i in self._in_infos
if len(i.shape) == 3 and i is not self._image_info]
if len(cands) != 1:
raise ValueError(
"HEF 输入中无法唯一识别动作历史 vstream(期望一个 3 维输入),"
f"实际: {[(i.name, tuple(i.shape)) for i in self._in_infos]}")
return cands[0]
def _derive_dims(self):
shape = tuple(self._image_info.shape)
if len(shape) == 5:
_, self.frame_len, c, self.image_size, _ = shape
if c != 3:
raise ValueError(f"图像输入通道维应为 3,实际形状 {shape}")
self._image_layout = "ncfhw"
else: # (1, S, S, F*3) NHWC 帧拼通道
_, self.image_size, s2, c = shape
if s2 != self.image_size or c % 3 != 0 or c == 3:
raise ValueError(
f"无法从 4 维图像输入推导帧数(期望 (1,S,S,F*3)),"
f"实际形状 {shape}")
self.frame_len = c // 3
self._image_layout = "nhwc_fused"
h_shape = tuple(self._history_info.shape)
self.history_len, self.action_dim = h_shape[1], h_shape[2]
out_shape = tuple(self._out_info.shape)
out_dim = out_shape[-1]
if out_dim != self.action_dim:
raise ValueError(
f"输出动作维度 {out_dim} 与动作历史维度 {self.action_dim} 不一致")
# ---------------- 生命周期 ----------------
def close(self):
self._stack.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
self.close()
return False
# ---------------- 推理 ----------------
def predict(self, images, action_history: np.ndarray) -> np.ndarray:
"""images: 长度为 frame_len 的 RGB 图像列表(旧→新,末帧为当前帧)
返回 (action_dim,) float32 动作向量。
"""
if not isinstance(images, (list, tuple)):
images = [images]
if len(images) != self.frame_len:
raise ValueError(
f"images 应为 {self.frame_len} 帧,实际为 {len(images)} 帧。"
"冷启动时可重复传入同一帧。")
action_history = np.asarray(action_history, dtype=np.float32)
expected = (self.history_len, self.action_dim)
if action_history.shape != expected:
raise ValueError(
f"action_history 形状应为 {expected},实际为 {action_history.shape}")
if self._action_mean is not None:
action_history = (action_history - self._action_mean) / self._action_std
from hailo_platform import FormatType
bindings = self._configured.create_bindings()
frames = np.stack([self._preprocess(img) for img in images])
bindings.input(self._image_info.name).set_buffer(
self._image_buffer(frames))
h_shape = tuple(self._history_info.shape)
if self._history_info.format.type == FormatType.UINT8:
q = self._history_info.quant_info
buf = np.clip(np.round(action_history / q.qp_scale + q.qp_zp),
0, 255).astype(np.uint8).reshape(h_shape)
else:
buf = action_history.astype(np.float32).reshape(h_shape)
bindings.input(self._history_info.name).set_buffer(buf)
out_shape = tuple(self._out_info.shape)
if self._out_info.format.type == FormatType.UINT8:
out_buf = np.zeros(out_shape, dtype=np.uint8)
else:
out_buf = np.zeros(out_shape, dtype=np.float32)
bindings.output(self._out_info.name).set_buffer(out_buf)
self._configured.run([bindings], timeout=10000)
result = np.asarray(
bindings.output(self._out_info.name).get_buffer(),
dtype=np.float32)
if self._out_info.format.type == FormatType.UINT8:
q = self._out_info.quant_info
result = (result - q.qp_zp) * q.qp_scale
result = result.reshape(-1)
if self._action_mean is not None:
result = result * self._action_std + self._action_mean
return result
def _preprocess(self, image) -> np.ndarray:
img = Image.fromarray(image) if isinstance(image, np.ndarray) else image
img = img.convert("RGB").resize(
(self.image_size, self.image_size), Image.BILINEAR)
return np.asarray(img, dtype=np.uint8) # HWC uint8
def _image_buffer(self, frames: np.ndarray) -> np.ndarray:
"""(F, S, S, 3) → HEF 图像输入缓冲区(按编译后布局)"""
if self._image_layout == "ncfhw":
return frames.transpose(0, 3, 1, 2)[np.newaxis] # (1,F,3,S,S)
# nhwc_fused: 帧沿通道拼接 (1, S, S, F*3)
return frames.reshape(1, self.image_size, self.image_size,
self.frame_len * 3)
def main():
parser = argparse.ArgumentParser(description="actpred HEF (Hailo) inference")
parser.add_argument("--hef", required=True, help="HEF 模型路径")
parser.add_argument("--images", required=True, nargs="+",
help="按时间顺序(旧→新)传入 frame_len 个图像路径")
parser.add_argument("--action-history", required=True,
help="冒号分隔各步,每步 action_dim 个值")
parser.add_argument("--stats", default=None,
help="action_stats.npy 路径(训练时启用归一化才需要)")
parser.add_argument("--output-format", choices=["json", "raw"],
default="json")
args = parser.parse_args()
with HailoPredictor(args.hef, stats_path=args.stats) as predictor:
if len(args.images) != predictor.frame_len:
raise SystemExit(
f"--images 需传入 {predictor.frame_len} 个路径(旧→新),"
f"实际为 {len(args.images)} 个")
images = [np.array(Image.open(p).convert("RGB")) for p in args.images]
history = parse_action_history(args.action_history, predictor.action_dim)
predicted = predictor.predict(images, history)
if args.output_format == "json":
print(json.dumps({"predicted_action": predicted.tolist()}, indent=2))
else:
print(" ".join(f"{v:.4f}" for v in predicted))
if __name__ == "__main__":
main()
+223
View File
@@ -10,7 +10,9 @@ import contextlib
import importlib.util import importlib.util
import io import io
import os import os
import sys
import tempfile import tempfile
import types
import unittest import unittest
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
@@ -808,5 +810,226 @@ class TestResumeCheckpoint(unittest.TestCase):
# ---------------- HailoPredictor 测试用 fake hailo_platform ----------------
class _FakeFormatType:
UINT8 = "UINT8"
FLOAT32 = "FLOAT32"
class _FakeFormat:
def __init__(self, fmt_type):
self.type = fmt_type
class _FakeQuantInfo:
def __init__(self, qp_zp=0.0, qp_scale=1.0):
self.qp_zp = qp_zp
self.qp_scale = qp_scale
class _FakeVStreamInfo:
def __init__(self, name, shape, fmt_type, quant=None):
self.name = name
self.shape = tuple(shape)
self.format = _FakeFormat(fmt_type)
self.quant_info = quant or _FakeQuantInfo()
class _FakeStream:
def __init__(self):
self._buffer = None
def set_buffer(self, buf):
self._buffer = buf
def get_buffer(self):
return self._buffer
class _FakeBindings:
def __init__(self):
self._streams = {}
def input(self, name=""):
return self._streams.setdefault(("in", name), _FakeStream())
def output(self, name=""):
return self._streams.setdefault(("out", name), _FakeStream())
class _FakeConfigured:
def __init__(self, transform):
self._transform = transform
def create_bindings(self):
return _FakeBindings()
def run(self, bindings_list, timeout=0):
for b in bindings_list:
self._transform(b)
class _FakeInferModel:
def __init__(self, transform):
self._configured = _FakeConfigured(transform)
@contextlib.contextmanager
def configure(self):
yield self._configured
class _FakeVDevice:
def __init__(self, transform):
self._transform = transform
def create_infer_model(self, path):
return _FakeInferModel(self._transform)
def __enter__(self):
return self
def __exit__(self, *exc):
return False
def _install_fake_hailo(test, infos_in, infos_out, transform):
"""向 sys.modules 注入 fake hailo_platform,无需硬件即可测试"""
fake = types.ModuleType("hailo_platform")
fake.FormatType = _FakeFormatType
fake.HEF = lambda path: types.SimpleNamespace(
get_input_vstream_infos=lambda: list(infos_in),
get_output_vstream_infos=lambda: list(infos_out))
fake.VDevice = lambda: _FakeVDevice(transform)
patcher = mock.patch.dict(sys.modules, {"hailo_platform": fake})
test.addCleanup(patcher.stop)
patcher.start()
def _first_row_transform(hist_name, out_name, quant_in=None, quant_out=None):
"""transform:输出 = 历史动作首行(量化边缘时先反量化再量化)"""
def transform(b):
hist = np.asarray(b.input(hist_name).get_buffer(), dtype=np.float32)
if quant_in is not None:
hist = (hist - quant_in.qp_zp) * quant_in.qp_scale
first = hist.reshape(-1, hist.shape[-1])[0]
if quant_out is not None:
first = np.clip(np.round(first / quant_out.qp_scale + quant_out.qp_zp),
0, 255).astype(np.uint8)
b.output(out_name).get_buffer()[...] = first
return transform
class TestHailoPredictor(unittest.TestCase):
"""HEF 推理封装测试(fake hailo_platform 注入,无需 Hailo 硬件)"""
def _frames(self, n, size=8):
return [np.random.randint(0, 255, (size, size, 3), dtype=np.uint8)
for _ in range(n)]
def test_float_ncfhw_layout(self):
from hailo_infer import HailoPredictor
infos_in = [
_FakeVStreamInfo("net/images", (1, 2, 3, 4, 4), _FakeFormatType.FLOAT32),
_FakeVStreamInfo("net/history", (1, 2, 3), _FakeFormatType.FLOAT32),
]
infos_out = [_FakeVStreamInfo("net/action", (1, 3), _FakeFormatType.FLOAT32)]
seen_shapes = {}
def transform(b):
seen_shapes["image"] = np.asarray(
b.input("net/images").get_buffer()).shape
_first_row_transform("net/history", "net/action")(b)
_install_fake_hailo(self, infos_in, infos_out, transform)
history = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32)
with HailoPredictor("fake.hef") as p:
self.assertEqual((p.frame_len, p.history_len, p.action_dim), (2, 2, 3))
self.assertEqual(p.image_size, 4)
result = p.predict(self._frames(2), history)
np.testing.assert_allclose(result, [1, 2, 3])
self.assertEqual(seen_shapes["image"], (1, 2, 3, 4, 4))
def test_uint8_quantized_roundtrip(self):
from hailo_infer import HailoPredictor
q_in = _FakeQuantInfo(qp_zp=128.0, qp_scale=0.01)
q_out = _FakeQuantInfo(qp_zp=10.0, qp_scale=0.5)
infos_in = [
_FakeVStreamInfo("net/images", (1, 2, 3, 4, 4), _FakeFormatType.UINT8),
_FakeVStreamInfo("net/history", (1, 2, 3), _FakeFormatType.UINT8, q_in),
]
infos_out = [_FakeVStreamInfo("net/action", (1, 3), _FakeFormatType.UINT8, q_out)]
_install_fake_hailo(
self, infos_in, infos_out,
_first_row_transform("net/history", "net/action", q_in, q_out))
history = np.array([[0.5, -0.25, 0.0], [1, 1, 1]], dtype=np.float32)
with HailoPredictor("fake.hef") as p:
result = p.predict(self._frames(2), history)
np.testing.assert_allclose(result, [0.5, -0.25, 0.0], atol=0.51)
def test_nhwc_fused_layout(self):
from hailo_infer import HailoPredictor
infos_in = [
_FakeVStreamInfo("net/images", (1, 4, 4, 6), _FakeFormatType.UINT8),
_FakeVStreamInfo("net/history", (1, 2, 3), _FakeFormatType.FLOAT32),
]
infos_out = [_FakeVStreamInfo("net/action", (1, 3), _FakeFormatType.FLOAT32)]
seen_shapes = {}
def transform(b):
seen_shapes["image"] = np.asarray(
b.input("net/images").get_buffer()).shape
_first_row_transform("net/history", "net/action")(b)
_install_fake_hailo(self, infos_in, infos_out, transform)
history = np.zeros((2, 3), dtype=np.float32)
with HailoPredictor("fake.hef") as p:
self.assertEqual(p.frame_len, 2) # 6 通道 = 2 帧 × 3
result = p.predict(self._frames(2), history)
np.testing.assert_allclose(result, [0, 0, 0])
self.assertEqual(seen_shapes["image"], (1, 4, 4, 6))
def test_frame_count_mismatch_raises(self):
from hailo_infer import HailoPredictor
infos_in = [
_FakeVStreamInfo("net/images", (1, 2, 3, 4, 4), _FakeFormatType.FLOAT32),
_FakeVStreamInfo("net/history", (1, 2, 3), _FakeFormatType.FLOAT32),
]
infos_out = [_FakeVStreamInfo("net/action", (1, 3), _FakeFormatType.FLOAT32)]
_install_fake_hailo(self, infos_in, infos_out,
_first_row_transform("net/history", "net/action"))
with HailoPredictor("fake.hef") as p:
with self.assertRaises(ValueError):
p.predict(self._frames(1), np.zeros((2, 3), dtype=np.float32))
def test_output_dim_mismatch_raises(self):
from hailo_infer import HailoPredictor
infos_in = [
_FakeVStreamInfo("net/images", (1, 2, 3, 4, 4), _FakeFormatType.FLOAT32),
_FakeVStreamInfo("net/history", (1, 2, 3), _FakeFormatType.FLOAT32),
]
infos_out = [_FakeVStreamInfo("net/action", (1, 4), _FakeFormatType.FLOAT32)]
_install_fake_hailo(self, infos_in, infos_out, lambda b: None)
with self.assertRaises(ValueError):
HailoPredictor("fake.hef")
def test_stats_normalization_roundtrip(self):
from hailo_infer import HailoPredictor
infos_in = [
_FakeVStreamInfo("net/images", (1, 2, 3, 4, 4), _FakeFormatType.FLOAT32),
_FakeVStreamInfo("net/history", (1, 2, 3), _FakeFormatType.FLOAT32),
]
infos_out = [_FakeVStreamInfo("net/action", (1, 3), _FakeFormatType.FLOAT32)]
_install_fake_hailo(self, infos_in, infos_out,
_first_row_transform("net/history", "net/action"))
with tempfile.TemporaryDirectory() as tmpdir:
stats_path = os.path.join(tmpdir, "action_stats.npy")
np.save(stats_path, np.stack([np.full(3, 1.0), np.full(3, 2.0)]))
history = np.array([[3, 3, 3], [1, 1, 1]], dtype=np.float32)
with HailoPredictor("fake.hef", stats_path=stats_path) as p:
result = p.predict(self._frames(2), history)
np.testing.assert_allclose(result, [3, 3, 3], atol=1e-6)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main(verbosity=2) unittest.main(verbosity=2)
@@ -0,0 +1,130 @@
# actpred × 树莓派 AI HAT 2(Hailo-10H)推理调研报告
日期:2026-08-13
设备:Raspberry Pi 5 + Raspberry Pi AI HAT 2(Hailo-10H,8GB RAM,40 TOPS)
## 1. 目标
调研如何利用 AI HAT 2 扩展板资源对 actpred 动作预测模型做推理加速,
验证板载软件栈可用性,并给出部署路径。
## 2. 环境实测结果
### 2.1 已安装的软件栈(apt 包)
| 包 | 版本 | 说明 |
|---|---|---|
| h10-hailort | 5.1.1 | Hailo 运行时库 |
| h10-hailort-pcie-driver | 5.1.1 | PCIe 驱动与固件 |
| hailo-h10-all | 5.1.1 | Hailo-10 元包 |
| hailo-tappas-core | 5.1.0 | TAPPAS 核心 |
| python3-h10-hailort | 5.1.1 | HailoRT Python API(`hailo_platform` 模块) |
| python3-hailo-tappas | 5.1.0 | TAPPAS Python 绑定 |
| rpicam-apps-hailo-postprocess | 1.12.0 | rpicam-apps 的 AI HAT 后处理插件 |
| hailo-models | 1.0.0 | 预装 HEF 模型集 |
> 注意:Python 模块名是 `hailo_platform`,**不是** `hailort`
> (`import hailort` 会 ModuleNotFoundError)。
### 2.2 硬件识别与基准
```bash
hailortcli scan # Device: 0001:01:00.0(正常识别)
hailortcli benchmark /usr/share/hailo-models/resnet_v1_50_h10.hef
# → resnet_v1_50: FPS: 307.59(温度均值 47.65°C)
```
### 2.3 预装 HEF 模型
`/usr/share/hailo-models/` 下预装了 `_h8l`/`_h10` 两套 HEF。
本机为 Hailo-10H,应使用 `_h10` 后缀模型:
resnet_v1_50、yolov8m、yolov8m_pose、yolov8s_pose、yolov5n_seg、yolov11m 等。
## 3. Python 推理 API 实测(关键发现)
hailort 5.x 对 Hailo-10 设备只支持**新版 infer API**:
- ❌ 旧版 `VDevice().configure(hef)` / `InferVStreams(hef, vdev)`
在 Hailo-10 上报 `HAILO_NOT_IMPLEMENTED (error 7)`(实测)
- ✅ 新版 `VDevice().create_infer_model(hef_path)` → `configure()` →
`create_bindings()` → `run(bindings, timeout)`(实测通过)
实测打通的最小推理流程:
```python
import numpy as np
from hailo_platform import VDevice, HEF
hef_path = "/usr/share/hailo-models/resnet_v1_50_h10.hef"
hef = HEF(hef_path)
in_info = hef.get_input_vstream_infos()[0] # (224,224,3) uint8
out_info = hef.get_output_vstream_infos()[0] # (1,1,1000) 量化 uint8
with VDevice() as vdev:
im = vdev.create_infer_model(hef_path)
with im.configure() as c:
b = c.create_bindings()
b.input(in_info.name).set_buffer(img_uint8_hwc)
b.output(out_info.name).set_buffer(
np.zeros(tuple(out_info.shape), dtype=np.uint8))
c.run([b], timeout=10000)
q = np.asarray(b.output(out_info.name).get_buffer()).astype(np.float32)
res = (q - out_info.quant_info.qp_zp) * out_info.quant_info.qp_scale
```
### 踩坑记录
1. `bindings.input(name)` / `bindings.output(name)` 不支持下标访问;
`get_buffer()` 在未 `set_buffer` 前返回 None,需先按
`HEF.get_input/output_vstream_infos()` 的 shape/dtype 自建 numpy 缓冲区。
2. 输出缓冲区 dtype/大小必须匹配 HEF 边缘格式(量化 uint8),
传 float32 会报 `Output buffer size ... different than expected`;
需手动用 `quant_info.qp_zp/qp_scale` 反量化。
3. `VStreamInfo` 无 `format_type` 属性,shape 与量化参数从
`get_input/output_vstream_infos()` 获取。
4. 摄像头实时推理走 `rpicam-hello/rpicam-vid --post-process-file ...`
(rpicam-apps-hailo-postprocess 提供)。
5. `hailo_platform.genai` 提供 LLM / VLM / Speech2Text 接口——
AI HAT 2(Hailo-10H + 8GB RAM)支持本地大模型,是与
AI HAT+(Hailo-8L)相比的最大增量。
## 4. actpred 部署路径
Hailo 板**不能直接运行 ONNX/PyTorch**,必须编译为 HEF(含量化):
```
actpred checkpoint (best_inference.pt)
→ export_onnx.py → model.onnx
→ Hailo DFC(Dataflow Compiler,需 x86 Linux 主机或云端 DFC)
量化校准(用 data/ 中的帧做 calibration set)
→ model.hef(拷回树莓派)
→ hailo_platform 推理(封装见 actpred/hailo_infer.py)
```
风险与注意点:
- **DFC 不在树莓派本机**(无 hailo/dfc 命令),需在 x86 主机安装或
使用 Hailo 云端 DFC 服务。
- actpred 是 Transformer 结构(多头自注意力),编译前需确认所用 DFC
版本对 attention 类算子的支持;遇不支持算子可拆分模型或换 op。
- actpred 仅 ~450K 参数,Pi 5 CPU(4×A76)直接 PyTorch/ONNX Runtime
推理也在可接受延迟范围;HAT 迁移的主要收益是释放 CPU 给 pybot
主控循环与图像采集。
- HEF 的输入/输出名与量化参数由 DFC 编译决定,推理封装按
`get_input/output_vstream_infos()` 动态适配,不硬编码名称。
## 5. 结论与产出
1. AI HAT 2 软件栈完整可用,新 API 推理链路实测打通(resnet50 307 FPS)。
2. actpred HEF 推理封装:`actpred/hailo_infer.py`(`HailoPredictor`),
接口对齐 `predict.ActionPredictorInference`。
3. 待办:在 x86 主机上用 DFC 将 actpred ONNX 编译为 HEF 并做量化校准,
随后用 `HailoPredictor` 替换 CPU 推理端做端到端验证。
## 参考
- [Raspberry-Pi-AI-HAT+ 使用教程](https://spotpear.cn/index.php/wiki/Raspberry-Pi-5-AI-HAT-Plus-Hailo-8L-13-26-Tops-PCIe-M.2.html)
- [树莓派5 Raspberry Pi AI HAT+2 官方 Hailo-10H 8GBRAM 40TOPS](https://spotpear.cn/index.php/shop/Raspberry-Pi-5-AI-HAT-Plus-2-Hailo-10H-40-Tops.html)
- [AI HAT+ 文档](https://shumeipai.nxez.com/documentation/accessories/ai-hat-plus.html)
- [Raspberry Pi AI Kit](https://www.waveshare.net/wiki/Raspberry_Pi_AI_Kit)
- [onnx 转为 hef 用树莓派 hailo 使用](https://wenku.csdn.net/answer/1e0n2rmysd)
@@ -0,0 +1,52 @@
# actpred 推理速度基准测试报告(Pi 5 CPU vs Hailo-10 NPU)
日期:2026-08-13
设备:Raspberry Pi 5(4×A76)+ AI HAT 2(Hailo-10H)
模型:actpred(~450K 参数,frame_len=3,history_len=5,action_dim=18)
## 1. 测试方法
- **PyTorch CPU**:`ActionPredictorInference(checkpoints/best_inference.pt)`,
输入 3 帧 640×480 随机图 + 全零动作历史,预热 3 次后取 20 次均值(含 PIL 预处理)
- **ONNX Runtime CPU**:`export_onnx.py` 导出后 ORT 加载,纯模型推理,
预热 3 次后取 50 次均值
- **Hailo-10 NPU**:板载 `resnet_v1_50_h10.hef`(25M 参数,比 actpred 大 ~55 倍),
新版 `create_infer_model` API 串行单帧,预热 10 次后取 200 次均值;
另有 `hailortcli benchmark` pipelined 吞吐
## 2. 结果
| 方案 | 延迟 | 吞吐 | 备注 |
|---|---|---|---|
| PyTorch CPU | 28.8 ms | 34.8 FPS | 含 PIL 图像预处理 |
| ONNX Runtime CPU | 5.9 ms | 168.7 FPS | 纯推理,float32 输入 |
| Hailo-10 NPU(resnet50,串行单帧) | 4.29 ms | 233 FPS | 含 Python/PCIe 开销 |
| Hailo-10 NPU(resnet50,pipelined) | — | 307 FPS | hailortcli benchmark |
## 3. 解读
1. resnet50 参数量约为 actpred 的 55 倍,NPU 上 actpred HEF 推理预计
**亚毫秒级**,NPU 不会成为瓶颈;串行单帧 4.29ms 中 Python/PCIe
开销占比高,闭环可用 `run_async` 流水线进一步压低。
2. 不迁移 NPU 也完全够用:ONNX Runtime 比 PyTorch 快近 5 倍(169 FPS),
是短期最优 CPU 方案。
3. HAT 的真实价值在于**把推理从 CPU 卸载**,让 4×A76 专注 pybot
主控循环与图像采集,而非绝对延迟。
## 4. 部署建议(按优先级)
1. **立即可用**:给 actpred 加 ONNX Runtime 后端(改动小,收益 ~5 倍)
2. **有 x86 环境后**:DFC 编译 HEF,走 `actpred/hailo_infer.py`
的 `HailoPredictor` 端到端验证
3. DFC 编译环境的说明:官方仅提供 x86_64 Linux wheel
(`hailo_dataflow_compiler-*-linux_x86_64.whl`,无 arm64 构建),
但 HEF 产物与主机架构无关,x86 上编译一次即可在树莓派长期使用;
无本机 x86 时可用云服务器临时实例或 QEMU 模拟(后者极慢,不推荐)。
编译环境调研详见
[actpred_hailo_hat2_inference_research_20260813.md](actpred_hailo_hat2_inference_research_20260813.md)
## 参考
- [张高兴的 Raspberry Pi AI 开发指南:将自定义模型编译为 Hailo NPU 的 .hef 模型](https://www.cnblogs.com/zhanggaoxing/articles/18597244.html)
- [Hailo Dataflow 编译器安装](https://blog.csdn.net/qq_40843129/article/details/148557199)
- [Hailo-8 系列——Hailo Dataflow Compiler 使用经验分享](https://blog.csdn.net/stoat04/article/details/137569246)