把导航模型放到服务端运行,传过去的往往不只是一张地图。激光观测、目标方向,以及沿途积累的状态,都可能带出机器人所在环境的信息。如果希望服务端帮忙计算,同时让这些输入保持加密,问题就从一次普通推理变成了一段需要持续推进的密态计算。
这次实验做的是局部导航:根据激光、相对目标和当前保持的动作,预测下一控制周期的线速度与角速度。我更关心的,是带记忆的策略能不能在多个控制周期里连续运行,隐藏状态一直留在密文中。
下面以 K2 recovered / original trial1 这个候选为主线。它完成了 40 场真实 FHE 仿真闭环。先说清楚这里的“真实”:推理执行了密码算术与自举,环境仍然是 IR-SIM;客户端与服务端在同机测试,尚未接入真实机器人、ROS 或跨机器服务。
先把隐私边界说清楚
同态加密允许计算方直接处理密文,再由持有私钥的一方解密结果。这里采用的 CKKS 面向近似数值运算,支持密文上的加法、乘法和相应的精度管理;背景可以从 CKKS 原始论文看起。
在这个系统的逻辑分工里,客户端持有私钥,负责观测和初始状态加密,以及最终动作解密。服务端持有明文模型参数与评估材料,持续维护密文隐藏状态和记忆投影,推理期间不需要解密它们。
激光 / 目标 / 当前保持动作
→ 客户端预处理与加密
→ 服务端密态策略 + 密文递归状态
→ 客户端解密动作
→ 下一控制周期执行预处理和动作执行仍在客户端侧。基准程序还会访问明文观测,用来运行独立参考、核对数值。这部分是验证设施,不能直接当成生产服务端的隐私边界。当前目标也不包含隐藏模型权重,或证明恶意服务端一定正确执行了计算。
导航输入里,还有正在执行的动作
每帧输入有 41 维,组成并不复杂:
| 输入 | 维度 | 含义 |
|---|---|---|
| 激光扇区 | 36 | 180 条射线裁剪到 0–7 米,每 5 条取最小值,再除以 7 |
| 相对目标距离 | 1 | min(distance / 10, 1) |
| 相对目标方向 | 2 | cos(angle) 与 sin(angle) |
| 当前保持动作 | 2 | 正在执行的归一化线速度与角速度 |
最后两维很重要。模型计算新动作时,环境还在执行旧命令,所以输入应当包含这段时间里真正保持的动作。把刚预测、尚未执行的新动作放进去,会改掉控制的因果顺序。
网络输出两维归一化动作。客户端先裁剪,再映射到速度:
a0, a1 = clip(action, -1, 1)
linear_velocity = 0.25 * (a0 + 1) m/s
angular_velocity = a1 rad/s默认范围分别是 0–0.5 m/s 和 −1–1 rad/s。裁剪由执行侧完成,密态电路没有执行这一步。核对密码计算时,比较的是裁剪前输出,避免让裁剪把电路误差藏起来。
快观测,慢记忆
递归网络的麻烦在于,上一帧留下的状态还要参与下一帧计算。这个候选采用一个很小的快慢网络:41 维输入编码成 32 维,GRU 隐藏状态为 16 维,动作头中间层为 32 维,最后输出两个动作值。神经参数共 5,378 个,其中快路径占 1,024 个。
公开周期设为 K=2,帧号从 0 开始。每帧都编码新观测,偶数帧更新 GRU 和记忆投影,间隔帧复用它们:
e_t = P_enc(W_enc x_t + b_enc)
if t mod 2 == 0:
h_t = PolyGRU(e_t, h_previous)
m_t = W_head h_t + b_head
else:
h_t = h_previous
m_t = m_previous
a_t = P_out(W_out P_head(m_t + V e_t) + b_out)h 是递归状态,m 是它通往动作头的投影。快路径 V e_t 每帧读取新观测,因此间隔帧依然可以改变动作。它只是暂缓更新记忆,并非重复上一条控制命令。
在 2 Hz 动作频率下,这相当于 1 Hz 更新记忆。改大 K 会改变策略看到信息的时间关系,通常需要重新训练和验证,不能把它当成一个不影响模型的性能开关。当前间隔帧的编码也没有保留到下一次 GRU 更新;两帧融合属于另一种实现,不能混用结果。
GRU 公式也要对齐
实现采用 PyTorch GRU 的 reset-after 约定。候选状态中的 reset 门作用在隐藏投影及其偏置之后:
n_t = P_n(W_in e_t + b_in
+ r_t ⊙ (W_hn h_previous + b_hn))这里不能换成先计算 r_t ⊙ h_previous、再做隐藏投影。移植时看起来相似的公式,可能已经对应不同的网络。原生 PyTorch、手写前向与独立 NumPy 参考因此需要先对齐。
把网络改写成密文上的计算
激活函数换成固定多项式
这条电路把 Tanh 和 Sigmoid 换成冻结的 Chebyshev 多项式,系数与定义域半径一起保存在配置中:
P(s) = Σ c_j T_j(s / R)| 位置 | 多项式次数 | 当前打包评估消耗 |
|---|---|---|
| encoder、r/z 门、candidate、output | 31 | 5 个 limbs |
| head | 15 | 4 个 limbs |
这是当前平衡 Chebyshev 求值实现的成本,包含叶子的明文系数乘法,不是所有多项式电路都通用的公式。各激活的系数和半径也不相同。
归一化常量 W/R、b/R 预先折进明文仿射参数,省去在线的额外归一化。定义域则仍然约束着模型:服务端无法直接读密文判断是否越界,也没有一层明文裁剪兜底。采样轨迹没越界,并不能证明任意输入都在范围内。
64 个逻辑槽,不是环维度
当前 Actor 使用 64 个逻辑槽,环维度为 65,536。前者描述这套打包布局,后者属于密码参数,不能混为一谈。
观测占前 41 槽,编码与记忆投影占前 32 槽,隐藏状态占前 16 槽。r/z 两路门放在不同窗口,使用向量系数在一次打包多项式调用中评估,再旋转 32 槽把 z 对齐。布局直接决定了哪些旋转、矩阵投影和密钥材料是必需的。
矩阵投影采用 bsgs_hoisted,baby step 为 8。同一份编码的多组投影可以共享 baby rotations 与分解工作,但各自的输出矩阵乘法仍然要执行。这些细节决定了一个“小网络”在密态下究竟有多小。
把自举放在能继续递归的位置
持续运行不能只盯着单帧输出。GRU 完成后,还得留下足够可用的密文状态供下一次递归使用。这里在更新帧结束 GRU 后刷新一份隐藏状态,再用它计算记忆投影。
自举是在密文上恢复后续计算预算的过程,不要求服务端拿到私钥。想对照基本流程,可以看 OpenFHE 的 CKKS 自举示例。以下数值则来自本实验后端的 cur_limbs,表示当前 limbs 数,不是已经消耗的乘法深度。
| 阶段 | 输出 limbs |
|---|---|
| 观测输入 | 21 |
| 输入仿射与 encoder 多项式后 | 15 |
| 完整 GRU 更新后 | 2 |
| 隐藏状态自举并对齐后 | 15 |
| 记忆投影 / 快路径投影 | 14 |
| 动作头多项式后 | 10 |
| 最终仿射与输出多项式后 | 4 |
刷新后的 h15 同时用于下次递归和当前动作头。间隔帧保留 h15 与 m14,只计算新编码和动作。动作计算产生新的临时密文,不把低层级结果写回缓存,因此读取缓存不会让它每帧继续掉层。
两类帧的工作量也不一样。下面的计数不展开自举内部算子:
| 操作 | 更新帧 | 间隔帧 |
|---|---|---|
| MatVec | 8 | 3 |
| 密文 × 密文 | 88 | 48 |
| 自举密文数 | 1 | 0 |
间隔帧省下 GRU 与自举,但编码和动作头仍然存在,整体成本不会简单除以二。
0.5 秒的周期,要按真实时钟算
环境以 20 Hz 推进物理仿真,每 10 个物理步进入一次控制边界,也就是 2 Hz。第 t 帧采样之后,机器人继续保持旧动作;新预测若及时到达,就在下一边界后开始执行。
0.5t 秒:采样观测,开始计算 a_t
→ 本周期继续执行旧命令
0.5(t+1) 秒:检查 a_t 是否按时到达
→ 合格则执行,否则保持旧命令并记录漏期初始保持动作是 [-1, 0],映射后为停车。worker 只接受同一 stream_id、来自当前帧减一、且接收时间不晚于截止期的命令。过期动作不会留到更晚的边界补执行。
“加速明文闭环”可以模拟相同的先后关系;实际时钟测试还会用独立仿真进程和 time.monotonic() 检查截止期。判断是否漏期,要看 worker 真正收到动作的时间,只看父进程什么时候发送还不够。
保持旧动作是一项时序规则。真实机器人在断网或持续漏期时应怎样减速、停车和恢复,需要另外设计。
状态推进,也需要提交边界
密态 Actor 不应该每一帧重新构造。一个 stream 从加密初始状态开始,帧号单调递增:
actor.reset_stream(stream_id, encrypted_h0)
result = actor.step(
encrypted_observation,
stream_id=stream_id,
frame_id=0,
)
encrypted_action = result["action"]这是调用关系示意,不包含密码上下文和密钥准备。即使初始隐藏状态全零,也需要提供合法的加密 h0。后续帧继续使用同一个 Actor,不能每帧 reset。
更新帧先在局部变量里算候选状态和记忆投影,完整动作头成功后,才一起提交 hidden、memory_head 与 next_frame。如果输出层抛异常,旧状态和帧号保持不变,可以重试同一帧。
但 GPU 已经做过的工作不会回滚。step() 成功后,如果客户端解码或消息发送失败,当前实现也不会自动恢复旧状态,或替重复请求缓存先前响应。Actor 内部提交、结果送达和动作生效,是三个需要分别处理的事件。
40 场闭环,能说明到哪一步
这组候选的归档记录了 40 场真实 FHE 闭环、1,362 帧,以及 690 次刷新。刷新次数按每个回合分别计算 ceil(T/K) 再相加,不能把所有回合的总帧数直接除以二。
| 本地处理时间 | 平均值 |
|---|---|
| 全部帧 | 129.55 ms |
| 更新帧 | 187.22 ms |
| 间隔帧 | 70.34 ms |
这里的本地处理时间是单帧加密、完整密态 Actor 调用、动作解密与提取三者相加,排除了初始化、预热和参考核验。它不是跨机器网络延迟。该组 2 Hz 闭环没有动作漏期,但约 130 ms 的平均值也不能直接证明更高控制频率可用;控制频率一变,策略时序和环境反馈都会变化。
另外,“执行了 40 场”并不等于“40 场全部成功到达”。导航表现还要看碰撞、超时、净空和路径代价,不能从密码计算时间或数值精度推导成功率。
验证需要分层
| 验证对象 | 要回答的问题 |
|---|---|
| PyTorch、手写前向、NumPy | GRU 门顺序、偏置和递归状态是否一致 |
| 精确激活与多项式明文 | 近似对动作、状态和闭环造成什么变化 |
| FHE 与同权重同系数参考 | 密码算术、打包和层级计划是否正确 |
| 帧序、复位与失败重试 | 持久状态和缓存能否持续推进 |
| 完整导航回合与实际时钟 | 导航质量、动作漏期和物理推进是否满足要求 |
预热会用非零私有 h0 检查动作和隐藏状态。正常闭环只解密动作,并与该回合自身观测序列上的多项式参考比较。参考没有接管控制,也不要求不同策略在轨迹分叉后还能逐帧一致。
接下来要补的,是系统边界
目前得到的是一个在既定维度、打包布局和时间协议下持续运行的实验系统。换权重、换多项式次数、改隐藏维度或 K,都需要重新检查数值、层级、完整轨迹与实际时钟,不能只替换一份文件就假设等价。
密码参数的最终格安全估计尚未完成,所以这组配置也不应直接标成经过完整 128-bit 安全审计的部署配置。跨机器密文协议、多会话调度、ROS 接口与网络失败后的状态同步,同样仍是后续工作。
对我来说,这次实验最值得保留的是一条完整的链路:模型怎样积累记忆,电路怎样留下可继续计算的密文状态,执行端又在什么时候采用动作。把这三件事对齐,才算开始接近一个能持续运行的隐私计算应用。