自定义算法¶
把一个新算法接入 plugrl-server,让它能被 plugrl-run-server 通过 UID 选择。
快速开始¶
- 在
plugrl-server/src/plugrl_server/algorithm/<algo_uid>/下新建包。 - 注册一个配置 dataclass 和一个算法类。
- 确保 server 启动时这两个模块都会被 import,Tyro 才能发现它们。
参考实现:plugrl-server/examples/sac/sac.py,一个 off-policy 的 SAC,配套策略在
sac_policy.py。在 plugrl-server 目录下用
python examples/sac/sac.py sac_policy default sac default 运行。
文件结构¶
下面两种组织方式都可以。
直接放进 plugrl-server¶
plugrl_server/algorithm/<algo_uid>/<algo_uid>_config.py:配置 dataclass,用@register_algo_config注册plugrl_server/algorithm/<algo_uid>/<algo_uid>.py:算法类,用@register_algo注册plugrl_server/algorithm/__init__.py:把这两个模块都 import 进来
两个都要 import。只 import 配置模块的话,UID 会出现在 CLI 里,但运行时打印完配置
就报 KeyError: 'Algorithm <algo_uid> is not registered.'。像
examples/sac/sac.py 那样把两个装饰器放在同一个模块里也可以。
放在你自己的包里¶
- 算法代码放进你自己的 Python 包。
- 进入
plugrl_server.cli:main之前先 import,配置和类都要。
Server 调用契约¶
WebSocket server loop 会调用这些方法。
infer(obs) -> (action, runtime_state):每批环境调用一次。derive_train_state(runtime_state) -> train_state:每次infer之后紧接着调用, 默认返回None。结果按环境切开,再作为feedback(train_state=...)传回来。example_train_state(n)会拿policy.fake_runtime_state(n)调它,内置算法就是 这样确定 buffer 大小的。feedback(...) -> (prev_node, global_step, log_dict):只对要存下来的帧调用。discard_feedback(*, info, next_terminated, next_truncated):其余每一帧都走这里。 默认实现只记录已结束 episode 的指标。覆写时要调用super().discard_feedback(...),否则这些 episode 不会出现在rollout/*里。learn() -> (global_step, log_dict):前面调pre_learn(),后面调post_learn()。基类的post_learn会清空 episode 指标,覆写时应调用super().post_learn()。- 调度与保存:
should_learn、should_save、should_stop、create_checkpoint
另外两个由 CLI 在 server 启动前调用。
init_optimizers():算法构建完立刻调用,默认什么也不做。dppo在这里构建优化器。load_checkpoint(checkpoint):带--resume启动时调用。
还有一个类属性。
on_policy: bool = True。只有动作出自当前策略、并且should_learn()为假的帧, server 才会交给feedback,其余都交给discard_feedback。从 replay buffer 学习的算法把on_policy设为False,就能拿到每一个带 step state 的帧,SAC 示例就是这样。见训练循环。
这个循环对各个钩子意味着什么。
should_learn()为真时 server 不做推理。如果learn()之后它仍然为真,server 会接着再 learn 一次,中间不推理。所以learn()或post_learn()必须清空 buffer,或者推进should_learn所依据的计数。should_save()和should_stop()在循环每转一圈时都会检查,而不是每次 learn 才查一次。create_checkpoint()执行之后should_save()必须变回假。内置算法都在create_checkpoint里记下已保存的轮数。
准确签名见 plugrl_server/algorithm/base_algorithm.py。server 调用的是
learn(),但 learn() 在 BaseAlgorithm 上已经实现了:它调用 learn_impl()
再用 build_train_info 包一层结果,rollout/* 指标就是在这里加上的。抽象方法是
learn_impl,要覆写的是它。覆写 learn 会让 learn_impl 悬空、类仍然是抽象的,
make_algo 会直接抛 TypeError。
infer 与 feedback 收发的是 plugrl_server.policy.state 里的
PolicyRuntimeState,feedback 还要接 train_state: PolicyTrainState = None。
旧模板里用过 InternalState 和 get_action_and_internal_state,这两个都不存在。
feedback 的每个参数都是 keyword-only,所以名字对不上会在 server 第一次调用时
直接 TypeError,不会被悄悄当成改名放过。
最小模板¶
import dataclasses
import numpy as np
from plugrl_server.algorithm.base_algorithm import BaseAlgoConfig, BaseAlgorithm
from plugrl_server.algorithm.registration import register_algo, register_algo_config
from plugrl_server.common.checkpoint_manager import Checkpoint
from plugrl_server.policy.base_torch_policy import BaseTorchPolicy
from plugrl_server.policy.state import PolicyRuntimeState, PolicyTrainState
UID = "your-algo"
@register_algo_config(UID)
@dataclasses.dataclass
class YourAlgoConfig(BaseAlgoConfig):
# The run length is BaseAlgoConfig's field, set with --algo.global-steps.
global_steps: int | None = 100_000
@register_algo(UID)
class YourAlgorithm(BaseAlgorithm):
# Set to False if the algorithm learns from a replay buffer.
on_policy = True
def __init__(self, config: YourAlgoConfig, policy: BaseTorchPolicy):
super().__init__(config=config, policy=policy)
self.global_step = 0
def infer(self, obs: dict) -> tuple[np.ndarray, PolicyRuntimeState]:
action, runtime_state = self.policy.get_action_and_runtime_state(obs)
return action, runtime_state
def feedback(
self,
*,
obs: dict,
runtime_state: PolicyRuntimeState,
train_state: PolicyTrainState = None,
terminated: bool,
truncated: bool,
next_obs: dict,
reward: float,
info: dict,
next_terminated: bool,
next_truncated: bool,
prev_node: tuple,
) -> tuple[tuple, int, dict]:
# Store the frame here.
if (next_terminated or next_truncated) and "episode" in info:
if bool(info["episode"].get("mask", True)):
# Without this, rollout/success, reward and length stay 0.
self.record_episode_metrics(info["episode"])
self.global_step += 1
return prev_node, self.global_step, {}
def learn_impl(self) -> tuple[int, dict]:
return self.global_step, {}
def should_learn(self) -> bool:
return False
def should_stop(self) -> bool:
return self.global_step >= self.config.global_steps
def should_save(self) -> bool:
return False
def create_checkpoint(self) -> Checkpoint:
# state_dict() exists because a BaseTorchPolicy is a torch.nn.Module.
return Checkpoint(step=self.global_step, model=self.policy.state_dict(), optimizer=None, meta={})
def load_checkpoint(self, checkpoint: Checkpoint) -> None:
self.global_step = checkpoint.step
if checkpoint.model is not None:
self.policy.load_state_dict(checkpoint.model)
设计规则¶
- 模型结构与动作生成参数放在 policy config。
- 运行长度用
BaseAlgoConfig.global_steps,不要另起一个字段。进度显示读的就是它。 learn()用外部数据时,显式维护环境步数与更新步数。- 训练调度相关计数写进
Checkpoint.meta,并在load_checkpoint恢复。 - 不写分布式钩子就用
BaseAlgorithm,不要直接上DDPAlgorithm。
验证¶
先跑一个 smoke test。
plugrl-run-server dummy-policy default your-algo default
plugrl-run-env-client dummy-v1 --server-host 127.0.0.1 --server-port 8000 --num-episodes 1
如果算法在外部包里,先 import 再进入 CLI。
python -c "import my_pkg.plugrl_algorithms; from plugrl_server.cli import main; main()" \
dummy-policy default your-algo default
常见问题¶
plugrl-run-server <policy> <variant> --help里找不到 UID:模块没有被 import。- 打印完配置后报
KeyError: 'Algorithm your-algo is not registered.':只 import 了配置模块,类所在的模块没 import。 rollout/*一直是 0:feedback没调用record_episode_metrics。如果只是少了一部分 episode,是覆写的discard_feedback没调用super()。- server 反复 learn、不再推理:
learn()之后should_learn()仍然为真。 - 循环每转一圈都写一次 checkpoint:
create_checkpoint()之后should_save()还是真。 --algo.*和--policy.*出现重复语义参数:保留一侧即可。- resume 后学习频率或保存周期漂移:从
Checkpoint.meta恢复所有计数。 DDPAlgorithm运行时报错:改用BaseAlgorithm,或补齐分布式钩子。