Files
blackbird/actpred/_onnx_validate.py
T
qing 9179281420 fix(pybot×actpred): v3 审计 P2 清零——验收链 fixed_range + 导出断言 + overrun 节流
- v3-P2-1: _npu_test/_onnx_validate 按 normalize_mode 分支(fixed_range 免 stats,
  HailoPredictor 透传模式,torch 参考同口径反归一化)
- v3-P2-2: export_deploy/export_onnx 一致性 diff 超 1e-4 即 sys.exit(P2-f 闭环)
- v3-P2-3: run_loop overrun 告警节流(第1次+每100次),降 hz 建议仅 >10% 输出
- P2-a1: default_column_weights 扩展列(action_dim>18)默认 1.0
- P2-c: 补 predictor 异常清理路径测试(异常传播 + finally 落盘)
- P2-d/P2-e: Resize 口径与 resume scheduler T_max 语义文档化
- R2-4: _set.sh 安装后断言 CPU 版 torch,误装 CUDA 即拦截
测试: pybot 89 passed (+1)、actpred 148 passed/18 skipped
2026-08-14 23:01:56 +08:00

139 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""部署版 ONNX 数值验证:输入敏感性 + 与 PyTorch 一致性(重编译前门槛)
用法: venv/bin/python _onnx_validate.py
"""
import sys
sys.path.insert(0, "/opt/blackbird/actpred")
import numpy as np
import torch
from PIL import Image
from torchvision import transforms
import onnxruntime as ort
from config import parse_checkpoint_config
from model import ActionPredictor
CKPT = "checkpoints/pretrain_best_inference.pt"
ONNX = "checkpoints/actpred_deploy.onnx"
IMG_DIR = "data/episode_0000/images"
ck = torch.load(CKPT, map_location="cpu", weights_only=True)
cfg = parse_checkpoint_config(ck["config"])
model = ActionPredictor(cfg.model, cfg.data, pretrained=False)
model.load_state_dict(ck["model_state_dict"])
model.eval()
action_stats = None
norm_mode = cfg.data.normalize_mode if cfg.data.normalize_actions else "none"
if norm_mode == "zscore":
import os
stats_path = cfg.data.action_stats_path or os.path.join(
os.path.dirname(os.path.abspath(CKPT)), "action_stats.npy")
if not os.path.isabs(stats_path):
stats_path = os.path.join(os.path.dirname(os.path.abspath(CKPT)),
stats_path)
action_stats = np.load(stats_path)
if action_stats.shape != (2, cfg.data.action_dim):
raise ValueError(f"action stats 形状应为 (2, {cfg.data.action_dim}),"
f"实际为 {action_stats.shape}: {stats_path}")
print(f"history 将 z-score 归一化({stats_path})")
elif norm_mode == "fixed_range":
print("history 将 fixed_range min-max 归一化(action_norm.py,免 stats)")
sess = ort.InferenceSession(ONNX)
in_names = [i.name for i in sess.get_inputs()]
print("ONNX inputs:", in_names)
h, w = cfg.data.image_height, cfg.data.image_width
tf = transforms.Compose([
transforms.Resize((h, w)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
imgs_real = [np.array(Image.open(f"{IMG_DIR}/000{i}.jpg").convert("RGB"))
for i in range(3)]
imgs_zero = [np.zeros_like(imgs_real[0]) for _ in range(3)]
hist_real = np.array([
[45, 0, 45, 0, 45, 0, 45, 0, 90, 90, 10, 200, 0, 0, 0, 0, 0, 0],
[45, 0, 45, 0, 45, 0, 45, 0, 90, 90, 0, 20, 0, 0, 0, 0, 0, 0],
[45, 0, 45, 0, 45, 0, 45, 0, 90, 90, 0, 0, 0, 0, 0, 0, 0, 0],
[45, 0, 45, 0, 45, 0, 45, 0, 90, 90, 0, 0, 0, 0, 0, 0, 0, 0],
[45, 0, 45, 0, 45, 0, 45, 0, 90, 90, 0, 0, 0, 0, 0, 0, 0, 0],
], dtype=np.float32)
hist_zero = np.zeros((5, 18), dtype=np.float32)
if norm_mode == "fixed_range":
from action_norm import normalize_action
hist_real = normalize_action(hist_real, cfg.data.action_dim)
hist_zero = normalize_action(hist_zero, cfg.data.action_dim)
elif action_stats is not None:
hist_real = (hist_real - action_stats[0]) / action_stats[1]
hist_zero = (hist_zero - action_stats[0]) / action_stats[1]
pred_learned = model.pred_token.data.unsqueeze(1).numpy() # (1,1,1,128)
pred_none = np.zeros((1, 1, 1, 128), dtype=np.float32)
# R2 方案一:imu_dim>0 时模型/ONNX 多了归一化姿态输入
imu_dim = cfg.data.imu_dim
imu_real = None
imu_zero = None
if imu_dim > 0:
from imu_norm import normalize_imu
# 原始姿态(roll=5°, pitch=-3°, 其余 0)→ 部署版输入布局 (1,1,1,imu_dim)
imu_real = normalize_imu([5.0, -3.0] + [0.0] * 6, imu_dim).reshape(
1, 1, 1, imu_dim)
imu_zero = np.zeros((1, 1, 1, imu_dim), dtype=np.float32)
def frames_nhwc(imgs):
"""列表帧 → (1,H,W,3) 归一化 float32 NHWC × frame_len"""
chw = np.stack([tf(Image.fromarray(im)).numpy() for im in imgs])
return [chw[i].transpose(1, 2, 0)[None] for i in range(len(imgs))]
def run_onnx(imgs, hist, pred, imu=None):
feeds = {f"frame{i}": f for i, f in enumerate(frames_nhwc(imgs))}
feeds["action_history"] = hist[None, None]
feeds["pred_token"] = pred
if imu_dim > 0:
feeds["imu"] = imu if imu is not None else imu_zero
return sess.run(None, feeds)[0].reshape(-1)
def run_torch(imgs, hist, imu=None):
x = torch.stack([tf(Image.fromarray(im)) for im in imgs]).unsqueeze(0)
hh = torch.tensor(hist).unsqueeze(0)
ii = None
if imu_dim > 0:
ii = torch.tensor(
imu if imu is not None else imu_zero).reshape(1, imu_dim).float()
with torch.no_grad():
return model(x, hh, ii).squeeze(0).numpy()
a = run_onnx(imgs_real, hist_real, pred_learned, imu_real)
b = run_onnx(imgs_zero, hist_real, pred_learned, imu_real)
c = run_onnx(imgs_real, hist_zero, pred_learned, imu_real)
d = run_onnx(imgs_zero, hist_zero, pred_learned)
p0 = run_onnx(imgs_real, hist_real, pred_none, imu_real)
print(f"onnx real_img+real_hist: {np.round(a[:6], 3)}...")
print(f"onnx pred_token=0 diff : max={np.abs(a - p0).max():.4f}")
print(f"img sensitivity (a-b): max={np.abs(a - b).max():.4f}")
print(f"hist sensitivity (a-c): max={np.abs(a - c).max():.4f}")
ok = np.abs(a - b).max() > 1e-4 and np.abs(a - c).max() > 1e-4
if imu_dim > 0:
e = run_onnx(imgs_real, hist_real, pred_learned, imu_zero)
print(f"imu sensitivity (a-e): max={np.abs(a - e).max():.4f}")
ref = run_torch(imgs_real, hist_real, imu_real)
print(f"torch reference : {np.round(ref[:6], 3)}...")
print(f"onnx vs torch max_diff : {np.abs(a - ref).max():.6e}")
ok = ok and np.abs(a - ref).max() < 1e-4
print("PASS" if ok else "FAIL")