解读 03 · policy.rs:ONNX 策略的加载、预热与校验
解读对象:
duck-control/src/policy.rs(570 行)
需要的前置:《什么鸭公开课》第 19 课(ONNX 合同)
模块开宗明义:"一切都在加载时校验,而不是在推理时"(policy.rs:9-13)。观测宽度错、动作数错、运行库缺失,都应该在机器人还站着、还能告诉你为什么的时候失败——而不是六十个 tick 之后、走路走到一半。
1. 三道门:探测、验形、预热
Policy::load(policy.rs:246)依序过三道门。第一道 ensure_runtime()(policy.rs:116):先用 libloading::Library::new 试探 ONNX Runtime 动态库能不能加载,结果存进 OnceLock 只探一次。为什么要抢在 ort 前面?因为 ort 在库缺失时不返回错误,而是在内部 expect 直接 panic(policy.rs:101-106),控制线程一死,健康检查只会永远报"循环没完成过一个 tick"——守护进程看着像卡死,而不是"没装库"。探针把缺库变成一条带修法的普通错误(ORT_DYLIB_PATH 或装库)。
第二道在 open() 里:check_width 检查图的输入宽度 61、输出数 14(policy.rs:377-378)。只查张量最后一维,因为前导维是 batch、通常是动态的 -1(policy.rs:383-385)。
第三道是预热:open_warm 打开后立刻用 Observation::zeroed() 跑一次零观测推理(policy.rs:257-262)。首次推理永远是离群值——惰性初始化、冷页、缺页中断——拖到第一个 tick 就像控制环超时。零观测"不是有效机器人状态",输出会被丢弃,只是把首次开销从热路径上买走(obs.rs:152-160)。
2. 把 panic 变成错误
catching_ort_panics(policy.rs:166)用 std::panic::catch_unwind 包住全部 ort 调用,panic 转成 PolicyError::RuntimePanic。这不是理论防御:真有一块板子装着 ONNX Runtime 1.20.1,库能加载(探针通过),ort 却在自己的版本检查上 panic——"expected >= 1.23.x, got 1.20.1"(policy.rs:111-115)。消息里的两个版本号就是全部诊断,所以 panic_message(policy.rs:177)想尽办法把消息捞出来带走。
两个前提注释都写明了:只包 ort 调用,不包整个 load,免得自己的 bug 被改写成"策略不可用"(policy.rs:155-157);根 Cargo.toml 没有 panic = "abort",加了它这层就静默失效(policy.rs:163-165)。
3. 网络选择与回退:宁可步态错,不可线程死
Net 枚举列出五类网络:Walk、Stand、SitStand、GroundPick、Skill(usize)(policy.rs:194-208)。走与站由速度指令的模长决定:will_stand(policy.rs:296)要求有 stand 网络、未被禁用、且模长不超过 DEFAULT_STANDING_THRESHOLD = 0.05(policy.rs:25)。单独提供这个查询,是因为调用方要根据同一答案选增益和动作缩放,"问两次不能得到两个答案"。
infer(policy.rs:322)里,请求了未加载的网络会回退到 walk 而不是 panic——注释称之为"错误的步态好过死掉的控制线程"。而 set_standing_disabled(policy.rs:288)是给滚轮模式和摔倒恢复用的:那些模式占用了站立网络,模长规则必须失效,否则零摇杆会把滚轮鸭切到一个为另一双腿训练的网络。
4. validate:回答一个问题,不开一场会
自由函数 validate(policy.rs:362)给 IPC 的 loadPolicy 用:打开图、验形状、立刻丢弃,不做预热。它证明的更少——"能打开且形状对"不等于"能跑"——但换来同步回答,把"接受了却没生效"变成一句即时的 observation width is 51, expected 61。注释还标了成本:开会话要几十毫秒,tick 只有 20,所以绝不在 tick 里调。
你带走的收获
- 动态库依赖要在使用方之前显式探测:FFI 库"缺失即 panic"时,不探测的代价是线程死亡加一条无名错误。
- 首次调用永远慢(惰性初始化、冷页),常驻循环的组件必须在计时开始前预热。
catch_unwind包最小范围:只包不可信的 FFI 边界,不把自己的 bug 也吞了。- 校验分两档:要运行的去
load(全验+预热),只问能不能用的去validate(验形即弃)。 - 错误要带"修法"而不是只带"案发现场":缺库错误指向环境变量,形状错误同时报两个数。