跳转至

通信协议

PlugRL 把一次训练拆成两个进程。训练服务端持有策略与学习算法,环境客户端 运行环境、请求动作、汇报结果。两者通过 WebSocket 通信,线上格式是 msgpack。

这条边界正是训练栈与环境栈不必共处同一个 Python 环境的原因,也是环境客户端 根本不必是 Python 的原因 —— 一个 ROS 节点可以是,机器人上的 C++ 控制器也可以是。

规范正本在 plugrl-protocol

SPEC.md 是规范性文件。它与所描述的代码放在一起,以免两者漂移;本页只做导览。

规范里用 Gap 标注了协议自己的已知缺陷。那部分才是诚实的部分, 在此之上开发前请先读它们。

一次交换

客户端                                      服务端
  |------------ WebSocket 握手 -------------->|
  |<---------------- metadata -----------------|   服务端先说话
  |------------------ infer ------------------>|   观测
  |<----------------- action ------------------|   一段动作块
  |----------------- feedback ---------------->|   奖励、终止标志、下一观测

四种消息类型 —— metadatainferactionfeedback —— 都是带 message_type 字段的 msgpack map。数组的形式是

{b"__ndarray__": true, b"data": <bin>, b"dtype": "<f4", b"shape": [4, 1, 7]}

dtype 是 numpy 的 typestr:一个字节序字符、一个类型字符、一个元素字节数。 用任何语言解析它大约十行代码。

四条最容易实现错的规则

消息严格交替。 inferactionfeedbackinfer……服务端的连接处理 是一段没有分发器的顺序代码,所以连发两个 infer 的客户端会让第二个被当成 feedback 解析,然后被断开。

一个周期里三条消息的环境集合不必相同。 infer 携带的是动作块刚用完的 环境,feedback 携带的是本步结束时动作块用完的环境。只要有一个环境提前终止, 这两个集合就永久不再相等。action 与其后 feedback 的配对只是流控, 不是语义关联 —— 服务端按环境编号查表路由。

奖励是整个动作块上的求和,不是最后一步的奖励。只汇报最后一步的客户端会在 一个不同的 MDP 上训练,而且不会有任何东西报错。

重连意味着从零开始。 服务端关于一个环境的全部记忆 —— 上一帧观测、策略的 step state、终止标志 —— 只活在一条连接里。重连的客户端必须丢弃手上未发出的 feedback:它描述的那次转移已经无法补全,发出去只会往训练缓冲里塞一条由空 观测拼出来的转移。这件事发生时两端都不会报错,所以才值得单独写一条。

检验一个实现

plugrl-protocol 附带一个服务端,它按规范逐条给客户端打分,有违规就以非零码退出。

刚 clone 下来的 plugrl-protocol 还差两步。conformance server 会 import websockets,但它不是声明的依赖 —— pyproject.toml 里只有 numpymsgpackuv sync 装不上它;另外 C++ 客户端是源码不是可执行文件,要先编译。CI 做的就是这两步:

g++ -std=c++17 -O2 -Wall -Wextra -o /tmp/plugrl_client examples/plugrl_client.cpp

uv run --with websockets examples/conformance_server.py --port 8000 --steps 20 &
/tmp/plugrl_client 127.0.0.1 8000 20

plugrl_client.cpp 用的是 POSIX socket(sys/socket.harpa/inet.h), 所以这一步需要 Linux、macOS 或 WSL。Python 参考客户端没有这个限制, 覆盖的条款是同一批:

uv run --with websockets examples/raw_client.py --host 127.0.0.1 --port 8000 --steps 20

报告分两个等级。violation 是真服务端会拒绝或处理错的问题;note 是真服务端 接受、但与 Python 客户端做法不同的地方 —— 是可移植性风险,不是违约。

两个参考客户端都能通过。raw_client.py 是 275 行 Python,只用 msgpackwebsockets,不用 numpy,也不用 PlugRL 的任何东西。plugrl_client.cpp 是 C++17, 完全不依赖第三方库:SHA-1、base64、WebSocket 分帧,以及协议需要的那部分 msgpack,全都写在同一个文件里 —— 因为嵌入式控制器面对的就是这种处境。

两者都在每次改动的 CI 里跑,所以本页的说法是被持续检验的,而不是被记住的。

与 openpi 的关系

PlugRL 的序列化就是 openpi 的。msgpack_numpy 取自 openpi(Apache-2.0),仅重新排版,因此数组编码逐字节相同 —— 用 C++ 或 Rust 写客户端时真正费力的那部分,可以在两个生态之间通用。

不同之处在消息层。openpi 发一个裸观测、收一个裸动作,这是服务一个策略所需的; PlugRL 给两者加了信封,并加上 feedback 回传通道,这是训练一个策略所需的。