fix(actpred): 落实 0812 代码审计 N-2/N-3 修复项

- 默认 batch_size 32→4(1920×1080 下 CPU/Pi 内存不可行)
- 速度基准报告补 1080p 复测注记(CPU 不可实时,需 NPU 或降分辨率)
- 附重写后首轮全量审计报告(含 N-1 checkpoint 兼容性回归记录)
This commit is contained in:
qing committed 2026-08-13 01:34:00 +00:00
1 parent 0d16832612
commit 2068319c3a
3 files changed
+371 -1

No files matched your search

@@ -4,6 +4,16 @@
设备:Raspberry Pi 5(4×A76)+ AI HAT 2(Hailo-10H)
模型:actpred(~450K 参数,frame_len=3,history_len=5,action_dim=18)
> ⚠️ **基线分辨率说明(2026-08-12 补注,N-3 修复)**
>
> 本报告 §1~§4 的全部数据均在 **640×480** 输入分辨率下测得。
> 提交 `91b3004`(2026-08-12)已将模块默认输入分辨率改为
> **1920×1080**(`config.py` 默认 `image_width/image_height`,与 pybot
> 相机采集分辨率对齐)。**推理延迟随输入像素量近似线性增长
> (1920×1080 ≈ 640×480 的 6.75 倍像素),本报告数字不能直接外推到
> 新默认分辨率。** x86 参考机上的 1920×1080 复测数据见 §5;
> Pi 5 / Hailo 板载在新分辨率下的数据待补测。
## 1. 测试方法
- **PyTorch CPU**:`ActionPredictorInference(checkpoints/best_inference.pt)`,
@@ -45,6 +55,31 @@
编译环境调研详见
[actpred_hailo_hat2_inference_research_20260813.md](actpred_hailo_hat2_inference_research_20260813.md)
## 5. 1920×1080 复测(2026-08-12,x86 参考机)
分辨率重构(`91b3004`)后在新默认分辨率下补测 CPU 部分
(Hailo 板载待有硬件环境后补测):
| 方案 | 输入分辨率 | 延迟 | 备注 |
|---|---|---|---|
| PyTorch CPU | 1920×1080 | 701.6 ms | 含 PIL 预处理,预热 2 次取 5 次均值 |
| ONNX Runtime CPU | 1920×1080 | 326.3 ms | 纯推理,预热 2 次取 10 次均值 |
测试机:Intel i3-3240(2 核 4 线程,2012 年款),7.7 GB 内存;
与 Pi 5(4×A76)**不可直接横向对比绝对值**,仅用于观察分辨率影响趋势。
**结论**:
1. ONNX 相对 PyTorch 仍快约 2.2 倍,§4 的部署优先级方向不变;
2. 即便在 x86 桌面 CPU 上,1920×1080 单帧推理也需数百毫秒,
**远不能满足实时闭环**(原 640×480 报告推算 Pi 5 上将超过 1 秒)。
1920×1080 全分辨率闭环推理必须依赖:
- Hailo HAT 2 NPU(uint8 量化 + 硬件加速,预期可压到几十毫秒级),或
- 训练/推理时降低实际分辨率(如 960×540 或 640×360),或
- 裁剪输入(ROI)后再推理;
3. 建议在 Pi 5 + HAT 2 环境补测 1920×1080(ONNX CPU / HEF NPU),
并据此确定量产部署分辨率。
## 参考
- [张高兴的 Raspberry Pi AI 开发指南:将自定义模型编译为 Hailo NPU 的 .hef 模型](https://www.cnblogs.com/zhanggaoxing/articles/18597244.html)
+333
View File
@@ -0,0 +1,333 @@
# actpred 模块代码审计报告(Transformer 架构重写后,三轮审查)
> 审查日期:2026-08-12 (Asia/Shanghai)
> 审查人:QoderCN Automated Review
> 审查范围:`actpred/` 全模块(Python,PyTorch 动作预测神经网络,注意力架构)
> 代码基线:第 1/2 轮 — `36feeb4 feat(actpred): 新增 ONNX Runtime 推理后端 OnnxPredictor`;
> 第 3 轮 — `91b3004 refactor(actpred): 模型输入尺寸 image_size → image_width/image_height`
>
> **与上轮审计的关系**:上一份报告 `actpred_CODE_AUDIT_20260808.md` 针对的是
> 旧架构(MLP/GRU 历史编码 + 融合头,~140K 参数)。此后模块经历了注意力架构重写
> (`docs/reports/actpred_attention_rewrite_test_20260809.md`),并新增了
> .mt 预训练(`pretrain_mt.py`)、Hailo NPU 后端(`hailo_infer.py`)、
> ONNX Runtime 后端(`onnx_infer.py`)。本报告为架构重写后的**首轮全量审计**,
> 含三轮审查:第 1 轮整体审计 + 第 2 轮深度复审(边界行为/资源管理/跨文件一致性)
> 见 §二~§七;第 3 轮针对分辨率重构提交(`91b3004`)的增量审计及 N-1 回归的
> 修复验证见 §八。上上轮报告(20260808)的 27 项发现已全部修复,
> 本报告仅记录新发现的问题。
---
## 一、模块概述
| 属性 | 值 |
|------|-----|
| 定位 | 机器人动作预测(多帧图像 + 历史动作 → 下一步动作回归) |
| 语言 | Python 3.12(venv 内置) |
| 框架 | PyTorch ≥ 2.0 |
| 架构 | CNN/MobileNetV2 图像编码 + Transformer(pre-LN)token 融合 + `[PRED]` token 回归 |
| 参数量 | ~450K(simple CNN 默认配置) |
| 入口 | `_run.sh`(train / pretrain / export / predict 子命令分发) |
### 目录结构
```
actpred/
├── config.py # 三组 dataclass 配置 + CLI 解析 + checkpoint 配置校验
├── model.py # ActionPredictor(SimpleCNN/MobileNetV2 + Transformer encoder)
├── dataset.py # ActionDataset + DataRecorder
├── train.py # 训练主流程(episode 级划分、早停、余弦退火、断点续训)
├── pretrain_mt.py # 从 .mt 脚本预训练动作动力学(无图像,零帧占位)
├── predict.py # PyTorch 推理后端
├── onnx_infer.py # ONNX Runtime CPU 推理后端(零 torch 依赖)
├── hailo_infer.py # Hailo NPU (HEF) 推理后端(零 torch 依赖)
├── export_onnx.py # ONNX 导出(opset 18,动态 batch)
├── mt_parser.py # .mt @Action 矩阵解析器(`-` 前向填充语义)
├── test_actpred.py # 单元测试(1133 行,含 fake hailo_platform 注入)
├── _run.sh / _set.sh # 运行 / 环境安装脚本
└── requirements.txt
```
### 总体评价
模块质量**较高**:
- checkpoint 反序列化全部使用 `weights_only=True`,杜绝 pickle RCE;
- `parse_checkpoint_config` 拒绝未知键、`validate_config_values` 限制值域,
可防恶意/损坏 checkpoint 注入异常维度(OOM 攻击面已收敛);
- 测试覆盖全面:模型前向、配置校验、.mt 解析、三后端端到端、Hailo fake 注入;
- 三个推理后端接口对齐(`predict(images, action_history)`),维度全部从
模型/HEF 元数据动态推导,无硬编码。
本轮共发现 **16 项**问题:0 严重、4 中、10 低、2 信息级。
---
## 二、发现清单(汇总)
| # | 严重度 | 位置 | 问题摘要 |
|---|--------|------|---------|
| R1-1 | 中 | `pretrain_mt.py:141` | `data_dir` 被覆盖为 .mt 目录,`--save-config` 导出语义错误的 JSON |
| R1-2 | 中 | `predict.py:92` / `onnx_infer.py:23` / `hailo_infer.py:34` | `parse_action_history` 重复实现三次 |
| R1-3 | 低 | `train.py:14-20` | `split_episodes` 在 n=1 时训练集为空,报错信息不明确 |
| R1-4 | 低 | `dataset.py:137-149` | `DataRecorder` 崩溃时产生孤儿 episode(有图无 actions.npy) |
| R1-5 | 低 | `predict.py:113-114` | `--action-dim` override 参数无用且是陷阱(详见 R2-9) |
| R1-6 | 低 | `_set.sh:8` | PyTorch 安装未固定版本,与 requirements.txt 约束不一致 |
| R1-7 | 信息 | `hailo_infer.py:206-210` | Hailo 后端不做 ImageNet 归一化(依赖 DFC 编译折叠),HEF 与代码强绑定 |
| R1-8 | 低 | 所有 CLI 路径参数 | 无目录遍历校验(本地工具可接受,暴露为服务需加固) |
| R2-1 | 中 | `train.py:158` / `pretrain_mt.py:250` | NaN 验证损失永久阻断 best checkpoint 保存 |
| R2-2 | 中 | `dataset.py:69-71` | `ActionDataset` 加载外部 stats 文件缺 shape 校验(推理端均有) |
| R2-3 | 中 | `train.py:103-105,193-201` | 空验证集时 `best.pt` 从不创建,`--init-from` 微调会找不到文件 |
| R2-4 | 中 | `_set.sh:11` | requirements.txt 重装时可能从镜像解析到 CUDA 版 torch |
| R2-5 | 低 | `dataset.py:77` | 归一化统计量仅从 target 计算,episode 前 start 步动作未参与 |
| R2-6 | 低 | `hailo_infer.py:65` | `HEF` 对象读取 vstream info 后从不释放 |
| R2-7 | 低 | `_run.sh:14-16` | 未知子命令静默回退到 `predict.py`,拼写错误难以发现 |
| R2-8 | 低 | `parse_action_history`(三处) | 非数值输入抛裸 `ValueError`,无 step/列上下文 |
| R2-9 | 低 | `predict.py:120` | `--action-dim` override 后必然在 shape 校验处失败,应删除 |
(编号:R1-x = 第 1 轮发现,R2-x = 第 2 轮复审新发现)
---
## 三、安全审查
### 已具备的防护(良好实践)
1. **pickle 反序列化**:全部 6 处 `torch.load` 均带 `weights_only=True`
(`train.py:120,140`、`predict.py:15`、`export_onnx.py:14`、
`pretrain_mt.py:227`、`model.py:50`)。
2. **checkpoint 配置校验**:`parse_checkpoint_config` 拒绝未知键
(含旧架构遗留键 `history_encoder`/`action_hidden_dim`),
`validate_config_values` 限制所有维度在合理范围(image_size ≤ 1024、
attn_dim ≤ 4096 等),防 OOM 注入。
3. **shell 脚本**:`_run.sh`/`_set.sh` 引号使用正确,无命令注入面。
### 残留风险(R1-8)
所有用户输入路径(`--data-dir`、`--model`、`--images`、`--action-stats-path`、
`--mt-dir`)均无目录遍历/白名单校验。当前 actpred 是本地机器人工具,风险可接受;
若未来通过 fsys/dnfs 等暴露为网络服务,需在入口层做路径规范化校验。
---
## 四、问题详述
### R2-1(中)NaN 验证损失永久阻断 best checkpoint 保存
**位置**:`train.py:158`、`pretrain_mt.py:250`
```python
if val_loss < best_val_loss: # NaN < x 恒为 False
```
**影响**:任一 epoch 出现 NaN(梯度爆炸、数据异常),即使后续 epoch 恢复正常,
best checkpoint 也永远不会再更新;NaN 也不触发 early stopping 的语义保护。
**建议修复**:
```python
if np.isfinite(val_loss) and val_loss < best_val_loss:
...
else:
if not np.isfinite(val_loss):
print(f"WARNING: val_loss 非有限值({val_loss}),跳过 best 更新")
```
### R2-2(中)`ActionDataset` 外部 stats 文件缺 shape 校验
**位置**:`dataset.py:65-71`
```python
stats = np.load(action_stats_path)
self._action_mean = stats[0] # 无 (2, action_dim) 校验
self._action_std = stats[1]
```
**对比**:三个推理后端(`predict.py:57`、`onnx_infer.py:69`、`hailo_infer.py:82`)
均有 `stats.shape != (2, action_dim)` 校验,唯独训练端缺失。错误 shape 会导致
`__getitem__` 中 broadcasting 产生静默错误数据或难懂的报错。
**建议修复**:补上与推理端一致的 shape 校验。
### R2-3(中)空验证集时 `best.pt` 从不创建
**位置**:`train.py:103-105, 158-173, 193-201`
验证集为空时 `validate()` 返回 `inf`,`inf < inf` 为 `False`,训练循环内
从不保存 best;结尾兜底分支只保存 `best_inference.pt`。用户若按 README
用 `best.pt` 做 `--init-from` 微调会得到文件不存在错误。
**建议修复**:空验证集警告中明确说明只会有 `best_inference.pt`,
或兜底同时保存 `best.pt`(与 last.pt 相同的完整结构)。
### R1-1(中)pretrain 覆盖 `data_dir` 为 .mt 目录
**位置**:`pretrain_mt.py:141`
```python
cfg.data.data_dir = args.mt_dir
```
`data_dir` 在预训练流程中未被使用,但 `--save-config` 导出的 JSON 会携带
语义错误的值(.mt 脚本目录被记为图像数据目录),后续加载该配置做微调
会指向错误位置。建议删除该行。
### R1-2(中)`parse_action_history` 三处重复
`predict.py:92`、`onnx_infer.py:23`、`hailo_infer.py:34` 完全相同。
`hailo_infer.py` 注释声明零 torch 依赖是有意为之,但三者均为纯 numpy 实现,
可抽到 `utils.py`(无 torch 导入)共享,同时修 R2-8 时只改一处。
### R2-4(中)`_set.sh` CUDA 版 torch 误装风险
**位置**:`_set.sh:8,11`
先从 CPU index 装 torch,但随后 `pip install -r requirements.txt -i tuna`
在升级/重装场景可能从清华镜像解析到 CUDA 版 torch(requirements.txt 无
CPU-only 约束)。对 Pi 5 等无 CUDA 环境会造成体积膨胀或运行异常。
**建议修复**:requirements.txt 顶部注释警示,或 `_set.sh` 对 torch 依赖
显式指定 `--index-url`(pip 支持 per-requirement index 需借助 constraints)。
### R1-7(信息)Hailo 与 PyTorch/ONNX 图像预处理差异
- PyTorch/ONNX 后端:`Resize → /255 → ImageNet normalize`(float32);
- Hailo 后端:直接传 uint8 原始像素,归一化由 DFC 编译阶段折叠进网络。
这是 Hailo 官方推荐做法且已有文档说明,但 **HEF 文件与本推理代码强绑定**:
手动替换未按正确 preprocessing 配置编译的 HEF,不会报错但结果全错。
测试已通过 fake `hailo_platform` 验证量化/反量化路径正确。
### 其余低优先级问题
| # | 详述 |
|---|------|
| R1-3 | `split_episodes`:n=1 时 `val_count=max(1,0)=1` 吃掉全部 episode,训练集为空。`train.py:99` 会抛错但信息不明。建议 split 内前置检查 `n >= 2`。 |
| R1-4 | `DataRecorder`:`actions.npy` 仅 `flush()` 时写入,崩溃留下孤儿 episode(加载器静默跳过,数据丢失)。建议 flush 失败时清理目录,或 record 时增量落盘。 |
| R1-5 / R2-9 | `predict.py --action-dim` override:checkpoint 已记录维度,override 后 `parse_action_history` 按新维度解析,随后 `predict()` shape 校验必然失败。建议删除该参数。 |
| R1-6 | `_set.sh` 装 torch 无版本约束,requirements.txt 为 `torch>=2.0,<3.0`,二者可能不一致。建议脚本改为 `pip install "torch>=2.0,<3.0" ...`。 |
| R2-5 | 归一化统计量从样本 target(`actions[start:]`)计算,episode 前 `max(H, F-1)` 步仅出现在历史中未参与统计。数据量大时可忽略。 |
| R2-6 | `hailo_infer.py:65` `HEF` 对象仅用于读 vstream info,之后不释放,可能持有句柄。 |
| R2-7 | `_run.sh` 未知子命令静默执行 `predict.py`,`bash _run.sh trani` 拼写错误报参数错而非提示未知命令。建议 `*) echo unknown; exit 1`。 |
| R2-8 | `parse_action_history` 非数值输入抛裸 `ValueError`,建议包装为带 step/列号的错误。 |
---
## 五、测试评估
**覆盖率**(相对源码 ~1500 行,测试 1133 行):
- ✅ 模型前向(多帧 / 单帧退化 / 动态 batch / 自定义 attn)
- ✅ 配置值域 + checkpoint 配置解析(未知键、缺段、旧架构拒绝、越界)
- ✅ .mt 解析(前向填充、首行 `-` 填 0、std.mt 跨模块回归锚点、坏文件跳过)
- ✅ 数据集(形状、归一化、帧数不一致警告、维度不匹配)
- ✅ 三后端端到端(PyTorch / ONNX 与 torch 前向对齐 / Hailo fake 注入含量化往返)
- ✅ 断点续训 checkpoint 格式与权重恢复
- ✅ ONNX 导出(缺 onnxscript 时自动跳过)
**缺口**:
- 无全链路集成测试(pretrain → train → export → OnnxPredictor 推理一致性);
- NaN/inf 损失路径无测试(对应 R2-1);
- `DataRecorder` 崩溃/部分写入场景无测试(对应 R1-4)。
---
## 六、修复优先级建议
| 优先级 | 问题 | 工作量 |
|--------|------|--------|
| 建议修 | R2-1(NaN 污染 best)、R2-2(stats shape 校验) | 小 |
| 值得修 | R1-1、R2-3、R2-4、R2-7、R1-5/R2-9 | 小 |
| 可选 | R1-2(抽共享 utils)、R1-3、R1-4、R1-6、R2-5、R2-6、R2-8 | 中 |
---
## 七、结论
actpred 在注意力架构重写 + 三后端部署扩展后,整体质量保持在较高水平:
安全基线(weights_only、配置校验)完好,测试体系完备,代码可读性好。
第 1/2 轮未发现严重缺陷;4 项中等问题(NaN best 阻断、stats shape 校验缺失、
空验证集 best.pt 缺失、CUDA torch 误装风险)均属健壮性/部署范畴,
建议在下一次训练迭代前修复。
第 3 轮(分辨率重构)发现 1 项**严重回归**(N-1:既有 checkpoint 无法加载),
已通过重新预训练策略修复并验证,见 §八。
---
## 八、第 3 轮审计:分辨率重构(`91b3004`)与 N-1 修复
### 8.1 变更概述
提交 `91b3004 refactor(actpred): 模型输入尺寸 image_size → image_width/image_height,默认 1920×1080`
配合 `a240377 feat(pybot): Camera 默认采集分辨率 640×480 → 1920×1080`,
将单一方形 `image_size`(默认 224)拆分为 `image_width`/`image_height`(默认 1920×1080),
与相机原生采集分辨率对齐。涉及全部 11 个源文件及测试。
### 8.2 第 3 轮发现
| # | 严重度 | 位置 | 问题摘要 | 状态 |
|---|--------|------|---------|------|
| N-1 | 严重 | `config.py:parse_checkpoint_config` | 旧 checkpoint 的 `image_size` 键被判为未知键,全部既有 checkpoint 无法加载 | ✅ 已修复(重训练策略) |
| N-2 | 中 | 默认 1920×1080 | 训练默认分辨率计算/内存开销大,CPU/Pi 上 batch=32 不可行 | ✅ 已修复(默认 batch_size 32 → 4) |
| N-3 | 低 | `docs/reports/actpred_inference_speed_benchmark_20260813.md` | 5.9ms 基准数据基于旧小分辨率,分辨率变更后失效 | ✅ 已修复(注明 640×480 基线 + 新增 §5 1920×1080 复测) |
### 8.3 N-1 详述与修复
**问题**:`parse_checkpoint_config` 的未知键拒绝机制(本身是安全特性)与配置
字段改名冲突——所有旧 checkpoint 的 `config['data']` 含 `image_size` 键,
新代码判定为未知键:
```
ValueError: checkpoint config['data'] 含未知键: ['image_size']
```
实测确认仓库内 `checkpoints/pretrain_best_inference.pt` 加载失败,波及
`predict.py --model`、`export_onnx.py --model`、`train.py --init-from/--resume`。
**修复策略**:重新训练(而非代码内做旧键迁移)。模型权重与输入分辨率无关
(`SimpleCNN`/`MobileNetEncoder` 均以 `AdaptiveAvgPool2d` 收尾),旧权重本身
可迁移,但选择重训练以获得干净的新格式 checkpoint:
1. 旧 checkpoint 归档至 `checkpoints/legacy_image_size_64/`(不删除,保留追溯);
2. 以与旧 checkpoint 相同参数重跑预训练:
`bash _run.sh pretrain --mt-dir ../bot4cj/scripts --epochs 200 --image-width 64 --image-height 64`
(预训练使用零帧占位图像,图像分支不参与学习,小分辨率仅为提速,
与旧 checkpoint 的 image_size=64 保持一致);
3. 训练在 epoch 101 早停(patience=15),best val_loss=205.77
(旧 checkpoint 为 190.58,同一小数据集上的正常波动)。
**验证结果**:
- 新 checkpoint config 为 `image_width: 64, image_height: 64`,无 `image_size` 键 ✅
- `ActionPredictorInference` 加载成功,推理输出形状 (18,)、数值有限 ✅
- `pretrain_best.pt` / `pretrain_best_inference.pt` / `pretrain_last.pt` 均已重新生成 ✅
- 全量测试套件 71/71 通过 ✅
**附注**:本次修复不改任何代码——`parse_checkpoint_config` 的严格未知键拒绝
是正确的安全设计,不应为兼容旧格式放宽;配置 schema 变更时重新生成 checkpoint
是更干净的做法。归档的 legacy checkpoint 仅供追溯,不应再用于推理。
### 8.4 重构正确性核查(无问题项)
- `transforms.Resize((H, W))`(torchvision 语义)与 PIL `resize((W, H))`
在 `dataset.py`/`predict.py` 与 `onnx_infer.py`/`hailo_infer.py` 中参数顺序均正确;
- Hailo 4D/5D 形状推导正确放宽方形约束(`_derive_dims` 删除 `s2 == image_size` 检查),
支持非方形输入;ONNX `_locate_inputs` 同步放宽;
- 重构无遗漏:源码中仅剩测试辅助函数 `_create_dummy_episode` 的局部参数名
`image_size`(与 config 字段无关,无害);
- README、配置表、docstring 中的形状标注(`(S,S)` → `(H,W)`)同步更新。
### 8.5 遗留事项(第 3 轮)
- **N-2 已修复**:`config.py` 默认 `batch_size` 32 → 4(1920×1080 单样本
激活+梯度约数百 MB,batch=4 是 CPU/Pi 8GB 的安全上限;小分辨率可经
`--batch-size` 调大),README 配置表同步更新;71/71 测试通过;
- **N-3 已修复**:基准报告顶部补注 640×480 基线说明,新增 §5 记录
1920×1080 复测(x86 参考机:PyTorch 701.6 ms / ONNX 326.3 ms)。
复测同时暴露新的部署级问题:**1920×1080 全分辨率 CPU 推理远不能
满足实时闭环**,量产必须依赖 Hailo NPU 量化推理或降低推理分辨率,
已在报告 §5 给出建议;Pi 5 板载新分辨率数据待补测;
- §六 优先级表中的第 1/2 轮遗留项(R2-1 NaN、R2-2 stats 校验等)仍未修复,
继续有效。