diff --git a/README.md b/README.md index 1197820..ea3f311 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,15 @@ PlugRL 文档站点源码,使用 MkDocs Material 构建,支持中英文双 本仓库使用 uv 管理 Python 环境与依赖。 ```bash -cd plugrl-docs +git clone https://github.com/PlugRL/plugrl.github.io.git +cd plugrl.github.io uv sync --frozen uv run mkdocs serve -a 127.0.0.1:8000 ``` +`plugrl-docs` 是 pyproject 里的发行包名,不是目录名;`git clone` 得到的目录叫 +`plugrl.github.io`。已经在仓库根目录时,跳过前两行即可。 + 在浏览器打开 http://127.0.0.1:8000/ 。 中文站点路径为 /zh/ 。 diff --git a/docs/algorithm/custom_algorithm.md b/docs/algorithm/custom_algorithm.md index e56b2f7..7a5089b 100644 --- a/docs/algorithm/custom_algorithm.md +++ b/docs/algorithm/custom_algorithm.md @@ -29,12 +29,26 @@ Put the code in one of these layouts. The WebSocket server loop calls these methods. -- `infer(obs) -> (action, internal_state)` +- `infer(obs) -> (action, runtime_state)` - `feedback(...) -> (prev_node, global_step, log_dict)` - `learn() -> (global_step, log_dict)` - Scheduling and checkpoint hooks: `should_learn`, `should_save`, `should_stop`, `create_checkpoint`, `load_checkpoint` -See `plugrl_server/algorithm/base_algorithm.py` for exact signatures. +See `plugrl_server/algorithm/base_algorithm.py` for exact signatures. The +server does call `learn()`, but `learn()` is concrete on `BaseAlgorithm`: it +calls `learn_impl()` and then wraps the result with `build_train_info`. +`learn_impl` is the abstract method, so that is the one you override. +Overriding `learn` instead leaves `learn_impl` unimplemented and the class +abstract, and `make_algo` fails with `TypeError`. An earlier version of the +template below did exactly that. + +`infer` and `feedback` take and return `PolicyRuntimeState` from +`plugrl_server.policy.state`; `feedback` also takes `train_state: +PolicyTrainState = None`. There is no `InternalState` type and no +`get_action_and_internal_state` method anywhere in `plugrl-server` - the +template used both names and neither imports. Every `feedback` parameter is +keyword-only, so a mismatched name is a `TypeError` on the server's first +call, not a silent rename. ## Minimal template @@ -46,7 +60,8 @@ 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_policy import BasePolicy, InternalState +from plugrl_server.policy.base_policy import BasePolicy +from plugrl_server.policy.state import PolicyRuntimeState, PolicyTrainState UID = "your-algo" @@ -63,15 +78,16 @@ class YourAlgorithm(BaseAlgorithm): super().__init__(config=config, policy=policy) self.global_step = 0 - def infer(self, obs: dict) -> tuple[np.ndarray, InternalState]: - action, internal_state = self.policy.get_action_and_internal_state(obs) - return action, internal_state + 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, - internal_state: InternalState | None, + runtime_state: PolicyRuntimeState, + train_state: PolicyTrainState = None, terminated: bool, truncated: bool, next_obs: dict, @@ -84,7 +100,7 @@ class YourAlgorithm(BaseAlgorithm): self.global_step += 1 return prev_node, self.global_step, {} - def learn(self) -> tuple[int, dict]: + def learn_impl(self) -> tuple[int, dict]: return self.global_step, {} def should_learn(self) -> bool: diff --git a/docs/algorithm/custom_algorithm.zh.md b/docs/algorithm/custom_algorithm.zh.md index b585362..db89220 100644 --- a/docs/algorithm/custom_algorithm.zh.md +++ b/docs/algorithm/custom_algorithm.zh.md @@ -29,12 +29,23 @@ WebSocket server loop 会调用这些方法。 -- `infer(obs) -> (action, internal_state)` +- `infer(obs) -> (action, runtime_state)` - `feedback(...) -> (prev_node, global_step, log_dict)` - `learn() -> (global_step, log_dict)` - 调度与保存:`should_learn`、`should_save`、`should_stop`、`create_checkpoint`、`load_checkpoint` -准确签名见 `plugrl_server/algorithm/base_algorithm.py`。 +准确签名见 `plugrl_server/algorithm/base_algorithm.py`。server 确实调用的是 +`learn()`,但 `learn()` 在 `BaseAlgorithm` 上已经实现了:它调用 `learn_impl()` +再用 `build_train_info` 包一层结果。抽象方法是 `learn_impl`,要覆写的是它。 +覆写 `learn` 会让 `learn_impl` 悬空、类仍然是抽象的,`make_algo` 会直接抛 +`TypeError`。下面这份模板此前正是这么写的。 + +`infer` 与 `feedback` 收发的是 `plugrl_server.policy.state` 里的 +`PolicyRuntimeState`,`feedback` 还要接 `train_state: PolicyTrainState = None`。 +`plugrl-server` 里没有 `InternalState` 这个类型,也没有 +`get_action_and_internal_state` 这个方法 - 模板里这两个名字都用了,而且都 import +不进来。`feedback` 的每个参数都是 keyword-only,所以名字对不上会在 server 第一次 +调用时直接 `TypeError`,不会被悄悄当成改名放过。 ## 最小模板 @@ -46,7 +57,8 @@ 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_policy import BasePolicy, InternalState +from plugrl_server.policy.base_policy import BasePolicy +from plugrl_server.policy.state import PolicyRuntimeState, PolicyTrainState UID = "your-algo" @@ -63,15 +75,16 @@ class YourAlgorithm(BaseAlgorithm): super().__init__(config=config, policy=policy) self.global_step = 0 - def infer(self, obs: dict) -> tuple[np.ndarray, InternalState]: - action, internal_state = self.policy.get_action_and_internal_state(obs) - return action, internal_state + 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, - internal_state: InternalState | None, + runtime_state: PolicyRuntimeState, + train_state: PolicyTrainState = None, terminated: bool, truncated: bool, next_obs: dict, @@ -84,7 +97,7 @@ class YourAlgorithm(BaseAlgorithm): self.global_step += 1 return prev_node, self.global_step, {} - def learn(self) -> tuple[int, dict]: + def learn_impl(self) -> tuple[int, dict]: return self.global_step, {} def should_learn(self) -> bool: diff --git a/docs/algorithm/index.md b/docs/algorithm/index.md index 7b196ce..629309b 100644 --- a/docs/algorithm/index.md +++ b/docs/algorithm/index.md @@ -44,9 +44,18 @@ Discovery. ## Built-in algorithms +Five UIDs are registered in `plugrl-server`. + +- `fpo`: FPO training loop - the algorithm the quickstarts run - `dummy`: protocol and connectivity smoke tests -- `dppo`: DPPO training loop -- `dppo-dist`: distributed DPPO via Ray launcher +- `eval`: run a policy without training it, optionally from + `--algo.policy-checkpoint-path` +- `dppo`: DPPO training loop, needs the `dppo` extra +- `dppo-dist`: distributed DPPO via the Ray launcher, needs the `dppo` extra + +Without the `dppo` extra installed, `plugrl-run-server` logs +`Could not import DPPO algorithm module` at startup and the `dppo-dist` +subcommand is absent. ## Troubleshooting diff --git a/docs/algorithm/index.zh.md b/docs/algorithm/index.zh.md index 9733173..acb70e5 100644 --- a/docs/algorithm/index.zh.md +++ b/docs/algorithm/index.zh.md @@ -44,9 +44,16 @@ plugrl-run-env-client dummy-v1 --num-episodes 1 ## 内置算法 +`plugrl-server` 里一共注册了五个 UID。 + +- `fpo`:FPO 训练循环 - 快速开始跑的就是它 - `dummy`:协议与联通性验证 -- `dppo`:DPPO 训练循环 -- `dppo-dist`:Ray 分布式 DPPO +- `eval`:只跑策略不训练,可用 `--algo.policy-checkpoint-path` 指定权重 +- `dppo`:DPPO 训练循环,需要 `dppo` 可选依赖 +- `dppo-dist`:经 Ray 启动的分布式 DPPO,需要 `dppo` 可选依赖 + +没装 `dppo` 可选依赖时,`plugrl-run-server` 启动会打印 +`Could not import DPPO algorithm module`,并且没有 `dppo-dist` 子命令。 ## 常见问题 diff --git a/docs/algorithm/ppo.md b/docs/algorithm/ppo.md index b4f7510..0aa1cbb 100644 --- a/docs/algorithm/ppo.md +++ b/docs/algorithm/ppo.md @@ -35,15 +35,32 @@ Key properties. ## Common options -- Tracking: `--track.enabled true`, `--track.tracker swanlab|wandb` -- Checkpoints: `--checkpoint-base-dir ./checkpoints`, `--resume true` +- Tracking: `--track.enabled`, `--track.tracker swanlab|wandb` +- Checkpoints: `--checkpoint-base-dir ./checkpoints`, `--resume` -Multi GPU runs via Ray launcher. +`--track.enabled` and `--resume` are bare boolean flags. Writing +`--track.enabled true` or `--resume true` is a parse error - tyro reports +`Unrecognized arguments: true` and exits. The off switches are +`--track.no-enabled` and `--no-resume`. Both were written with a `true` +argument on this page. + +Multi GPU runs via the Ray launcher. ```bash -plugrl-run-server-ray dppo-policy default dppo hopper --num-ddp-gpus 4 +plugrl-run-server-ray dppo-policy default dppo-dist hopper --num-ddp-gpus 4 ``` +!!! note "The Ray launcher is not a supported path today" + + The algorithm has to be `dppo-dist`, not `dppo`: `cli_ray.py` asserts + `isinstance(algo, DDPAlgorithm)`, and only `DPPOAlgoDistributed` under the + UID `dppo-dist` mixes `DDPAlgorithm` in. This page previously showed + `dppo`, which trips that assertion. The launcher also requires the `dppo` + extra, builds its worker list from the *local* GPU count - so a multi-node + cluster still only sees the head node - and its server speaks an older + dialect of the protocol than the WebSocket one. Use `plugrl-run-server` + unless you are working on the Ray path itself. + ## Troubleshooting - `--resume` does nothing: confirm the experiment directory already contains checkpoints. diff --git a/docs/algorithm/ppo.zh.md b/docs/algorithm/ppo.zh.md index 854f166..477f33b 100644 --- a/docs/algorithm/ppo.zh.md +++ b/docs/algorithm/ppo.zh.md @@ -35,15 +35,30 @@ server 以调度循环驱动训练。 ## 常用参数 -- 指标追踪:`--track.enabled true`、`--track.tracker swanlab|wandb` -- checkpoint:`--checkpoint-base-dir ./checkpoints`、`--resume true` +- 指标追踪:`--track.enabled`、`--track.tracker swanlab|wandb` +- checkpoint:`--checkpoint-base-dir ./checkpoints`、`--resume` + +`--track.enabled` 与 `--resume` 是不带值的布尔开关。写成 +`--track.enabled true` 或 `--resume true` 会直接解析失败 - tyro 报 +`Unrecognized arguments: true` 并退出。关掉它们用 `--track.no-enabled` +与 `--no-resume`。本页此前这两处都多写了一个 `true`。 多 GPU 训练使用 Ray 启动。 ```bash -plugrl-run-server-ray dppo-policy default dppo hopper --num-ddp-gpus 4 +plugrl-run-server-ray dppo-policy default dppo-dist hopper --num-ddp-gpus 4 ``` +!!! note "Ray 启动器目前不是受支持的路径" + + 算法必须写 `dppo-dist` 而不是 `dppo`:`cli_ray.py` 里有 + `isinstance(algo, DDPAlgorithm)` 断言,而只有 UID 为 `dppo-dist` 的 + `DPPOAlgoDistributed` 混入了 `DDPAlgorithm`。本页此前写的是 `dppo`, + 会卡在这条断言上。这个启动器还需要 `dppo` 可选依赖,它按*本机* GPU 数量 + 构造 worker 列表 - 所以多节点集群也只看得见头节点 - 而且它的 server 说的 + 是比 WebSocket 那套更旧的协议方言。除非你就是在改 Ray 这条路径,否则请用 + `plugrl-run-server`。 + ## 常见问题 - `--resume` 没生效:确认实验目录里已经有 checkpoint。 diff --git a/docs/env/custom_env.md b/docs/env/custom_env.md index f909e56..da4eea9 100644 --- a/docs/env/custom_env.md +++ b/docs/env/custom_env.md @@ -24,10 +24,19 @@ class CustomConfig(BaseEnvConfig): @register_env(UID) class CustomEnv(BaseEnv): - def __init__(self, config: CustomConfig, worker_id: int | None = None, total_workers: int | None = None): - super().__init__(config=config) - self.worker_id = worker_id - self.total_workers = total_workers + def __init__( + self, + config: CustomConfig, + num_envs: int = 1, + process_id: int | None = None, + total_processes: int | None = None, + ): + super().__init__( + config=config, + num_envs=num_envs, + process_id=process_id, + total_processes=total_processes, + ) def prepare_obs(self, obs: np.ndarray) -> Observation: return Observation(images={}, states={}, text="") @@ -60,6 +69,12 @@ plugrl-run-env-client custom-v1 --num-episodes 1 - Config inherits `BaseEnvConfig`. - Implement `reset` and `step`. - Convert raw env outputs into `Observation` in `prepare_obs`. +- `__init__` takes `config, num_envs, process_id, total_processes`, the same + four as `BaseEnv.__init__` and as the shipped `MuJoCoEnv`. `EnvSpec.make` + always passes `num_envs`, and `gym.make_vec` forwards `process_id` and + `total_processes`. An earlier version of this page used `worker_id` and + `total_workers`; those names appear nowhere in `plugrl-env-client`, and a + class with that signature raises `TypeError` on the unexpected `num_envs`. ## Registration @@ -70,7 +85,7 @@ plugrl-run-env-client custom-v1 --num-episodes 1 ## Troubleshooting - Env ID not listed: module import did not run. -- Multi process init conflicts: try `--use-env-lock`. +- Multi process init conflicts: try `--runner.use-env-lock`. ## Next steps diff --git a/docs/env/custom_env.zh.md b/docs/env/custom_env.zh.md index 83c2e99..91905c4 100644 --- a/docs/env/custom_env.zh.md +++ b/docs/env/custom_env.zh.md @@ -25,10 +25,19 @@ class CustomConfig(BaseEnvConfig): @register_env(UID) class CustomEnv(BaseEnv): - def __init__(self, config: CustomConfig, worker_id: int | None = None, total_workers: int | None = None): - super().__init__(config=config) - self.worker_id = worker_id - self.total_workers = total_workers + def __init__( + self, + config: CustomConfig, + num_envs: int = 1, + process_id: int | None = None, + total_processes: int | None = None, + ): + super().__init__( + config=config, + num_envs=num_envs, + process_id=process_id, + total_processes=total_processes, + ) def prepare_obs(self, obs: np.ndarray) -> Observation: return Observation(images={}, states={}, text="") @@ -61,6 +70,11 @@ plugrl-run-env-client custom-v1 --num-episodes 1 - config 继承 `BaseEnvConfig` - 实现 `reset` 与 `step` - 在 `prepare_obs` 中把原始输出转成 `Observation` +- `__init__` 接收 `config, num_envs, process_id, total_processes` 四个参数, + 与 `BaseEnv.__init__` 以及内置的 `MuJoCoEnv` 一致。`EnvSpec.make` 总会传 + `num_envs`,`gym.make_vec` 会转发 `process_id` 与 `total_processes`。本页 + 此前用的是 `worker_id` 与 `total_workers`,这两个名字在 `plugrl-env-client` + 里根本不存在,按那个签名写的类会因为多出来的 `num_envs` 直接抛 `TypeError`。 ## 注册 @@ -71,7 +85,7 @@ plugrl-run-env-client custom-v1 --num-episodes 1 ## 常见问题 - CLI 找不到 env id:模块没有被 import。 -- 多进程初始化冲突:可尝试 `--use-env-lock`。 +- 多进程初始化冲突:可尝试 `--runner.use-env-lock`。 ## 下一步 diff --git a/docs/env/index.md b/docs/env/index.md index 87d3ce1..1963da3 100644 --- a/docs/env/index.md +++ b/docs/env/index.md @@ -24,15 +24,30 @@ plugrl-run-env-client dummy-v1 --num-episodes 1 --server-host 127.0.0.1 --server ## How environments are created -Env client creates envs via Gymnasium. +Env client creates envs with `gym.make_vec`. This is the call in +`plugrl_env_client/runner/run.py`. ```py -env = gym.make(env_id, config=config_dataclass, max_episode_steps=max_episode_steps) +env = gym.make_vec( + env_id, + num_envs=num_envs, + vectorization_mode="vector_entry_point", + config=config_dataclass, + max_episode_steps=max_episode_steps, + process_id=process_id, + total_processes=total_processes, +) ``` +`register_env` registers each env with `entry_point=None` and only a +`vector_entry_point`, so plain `gym.make(env_id, ...)` fails with +` registered but entry_point is not specified`. This page previously +showed the `gym.make` form; that form never worked. + ## Built-in environment IDs - `dummy-v1` +- `mujoco-v1` - needs the `mujoco` extra; this is the env the quickstart uses - `classic-v1` - `atari-v1` - `robomimic-v1` @@ -41,14 +56,17 @@ env = gym.make(env_id, config=config_dataclass, max_episode_steps=max_episode_st ## Common env client flags -- `--num-workers`: run multiple env client processes +- `--num-procs`: run multiple env client processes - `--server-host`, `--server-port`: server address -- `--use-real-time`, `--fps`: fixed FPS for debugging +- `--recorder.video-fps`: output fps for recorded mp4 artifacts + +There is no flag for running an env at a fixed wall-clock FPS. `--use-real-time` +and `--fps` were listed here and do not exist on this CLI. ## Troubleshooting - Env ID not found in CLI: registration module was not imported. -- Multi process init conflicts: try `--use-env-lock` if your env is heavy. +- Multi process init conflicts: try `--runner.use-env-lock` if your env is heavy. ## Next steps diff --git a/docs/env/index.zh.md b/docs/env/index.zh.md index f6078c0..7f27452 100644 --- a/docs/env/index.zh.md +++ b/docs/env/index.zh.md @@ -26,15 +26,30 @@ plugrl-run-env-client dummy-v1 --num-episodes 1 --server-host 127.0.0.1 --server ## 环境如何创建 -env client 通过 Gymnasium 创建 env。 +env client 用 `gym.make_vec` 创建 env,下面就是 +`plugrl_env_client/runner/run.py` 里的调用。 ```py -env = gym.make(env_id, config=config_dataclass, max_episode_steps=max_episode_steps) +env = gym.make_vec( + env_id, + num_envs=num_envs, + vectorization_mode="vector_entry_point", + config=config_dataclass, + max_episode_steps=max_episode_steps, + process_id=process_id, + total_processes=total_processes, +) ``` +`register_env` 注册时 `entry_point=None`,只提供 `vector_entry_point`, +所以直接调用 `gym.make(env_id, ...)` 会报 +` registered but entry_point is not specified`。本页此前写的是 +`gym.make` 形式,那个写法从来跑不通。 + ## 常见内置环境 ID - `dummy-v1` +- `mujoco-v1`:需要 `mujoco` 可选依赖;快速开始用的就是它 - `classic-v1` - `atari-v1` - `robomimic-v1` @@ -43,14 +58,17 @@ env = gym.make(env_id, config=config_dataclass, max_episode_steps=max_episode_st ## 常用 env client 参数 -- `--num-workers`:多进程并行跑环境 +- `--num-procs`:多进程并行跑环境 - `--server-host`、`--server-port`:server 地址 -- `--use-real-time`、`--fps`:固定 FPS 运行 +- `--recorder.video-fps`:录制 mp4 的输出帧率 + +没有让环境按固定墙钟 FPS 运行的参数。本页此前列出的 `--use-real-time`、 +`--fps` 在这个 CLI 上并不存在。 ## 常见问题 - CLI 找不到 env id:注册模块没有被 import。 -- 多进程初始化冲突:环境较重时可尝试 `--use-env-lock`。 +- 多进程初始化冲突:环境较重时可尝试 `--runner.use-env-lock`。 ## 下一步 diff --git a/docs/policy/custom_policy.md b/docs/policy/custom_policy.md index 2a51c8e..d2ffec9 100644 --- a/docs/policy/custom_policy.md +++ b/docs/policy/custom_policy.md @@ -11,7 +11,11 @@ Define a config dataclass and a `BasePolicy` subclass, then register both. ```py import dataclasses -from plugrl_server.policy.base_policy import BasePolicy, BasePolicyConfig, InternalState +from plugrl_server.policy.base_policy import ( + BasePolicy, + BasePolicyConfig, + PolicyRuntimeState, +) from plugrl_server.policy.registration import register_policy, register_policy_config UID = "your-policy" @@ -28,10 +32,10 @@ class YourPolicy(BasePolicy): def prepare_observation(self, obs: dict): ... - def get_action_and_internal_state(self, obs: dict): + def get_action_and_runtime_state(self, obs: dict): ... - def fake_internal_state(self, batch_size: int) -> InternalState: + def fake_runtime_state(self, batch_size: int) -> PolicyRuntimeState: ... ``` @@ -66,15 +70,19 @@ python -c "import my_pkg.plugrl_policies; from plugrl_server.cli import main; ma ## Contract -- `prepare_observation` converts worker obs dict into tensors. -- `get_action_and_internal_state` returns an action and an `InternalState`. -- `fake_internal_state` returns shapes and dtypes that match your buffers. +- `prepare_observation` converts the worker obs dict into a `NumpyState` - + an `np.ndarray` or a nested mapping of them. `BaseTorchPolicy` converts that + to tensors for you in `extract_model_obs_tensor`. +- `get_action_and_runtime_state` returns an action and a `PolicyRuntimeState`. +- `fake_runtime_state` returns shapes and dtypes that match your buffers. +- `PolicyRuntimeState` is a type alias, not a base class: return whatever your + algorithm needs - a dict, a dataclass, or `None`. ## Troubleshooting - Policy UID not listed: module import did not run. - Training fails due to shape mismatch: keep action shape stable across infer and training. -- `InternalState` missing fields: align it with what your algorithm stores. +- Runtime state missing fields: align it with what your algorithm stores. - Device and dtype drift: move tensors to `self.device` and keep dtypes stable. ## Next steps diff --git a/docs/policy/custom_policy.zh.md b/docs/policy/custom_policy.zh.md index d973daa..7454589 100644 --- a/docs/policy/custom_policy.zh.md +++ b/docs/policy/custom_policy.zh.md @@ -11,7 +11,11 @@ ```py import dataclasses -from plugrl_server.policy.base_policy import BasePolicy, BasePolicyConfig, InternalState +from plugrl_server.policy.base_policy import ( + BasePolicy, + BasePolicyConfig, + PolicyRuntimeState, +) from plugrl_server.policy.registration import register_policy, register_policy_config UID = "your-policy" @@ -28,10 +32,10 @@ class YourPolicy(BasePolicy): def prepare_observation(self, obs: dict): ... - def get_action_and_internal_state(self, obs: dict): + def get_action_and_runtime_state(self, obs: dict): ... - def fake_internal_state(self, batch_size: int) -> InternalState: + def fake_runtime_state(self, batch_size: int) -> PolicyRuntimeState: ... ``` @@ -66,15 +70,18 @@ python -c "import my_pkg.plugrl_policies; from plugrl_server.cli import main; ma ## 约定 -- `prepare_observation` 把 worker 观测 dict 转成张量 -- `get_action_and_internal_state` 返回动作与 `InternalState` -- `fake_internal_state` 返回能用于 buffer 预分配的形状与 dtype +- `prepare_observation` 把 worker 观测 dict 转成 `NumpyState`,也就是 `np.ndarray` + 或它们的嵌套 mapping;转张量由 `BaseTorchPolicy.extract_model_obs_tensor` 负责 +- `get_action_and_runtime_state` 返回动作与 `PolicyRuntimeState` +- `fake_runtime_state` 返回能用于 buffer 预分配的形状与 dtype +- `PolicyRuntimeState` 是类型别名而不是基类:dict、dataclass 或 `None` 都可以, + 按算法需要返回 ## 常见问题 - CLI 找不到 UID:模块没有被 import。 - 训练时 shape 对不上:infer 与训练路径的动作形状必须一致。 -- `InternalState` 字段缺失:与算法写入 buffer 的字段对齐。 +- runtime state 字段缺失:与算法写入 buffer 的字段对齐。 - device 与 dtype 漂移:观测张量放到 `self.device` 并统一 dtype。 ## 下一步 diff --git a/docs/policy/dppo_policy.md b/docs/policy/dppo_policy.md index 53744e2..640506e 100644 --- a/docs/policy/dppo_policy.md +++ b/docs/policy/dppo_policy.md @@ -78,18 +78,23 @@ Common flags. Subclass `BasePolicyGradientDiffusionPolicy` in `plugrl-server/src/plugrl_server/policy/base_policy_gradient_diffusion_policy.py`. -The base class drives the denoising loop and fills `InternalState`: +The base class drives the denoising loop and fills a `DiffusionRuntimeState`: - Per-step: `action`, `logprob`, `entropy`, plus `obs["x"]` and `obs["t"]`. -- Final: calls `_postprocess_action` and stores `value` into `internal_state.value`. +- Final: calls `_postprocess_action` and stores `value` into `runtime_state.value`. What you implement. - `_get_timesteps`, `_initialize_x`, `_denoising_step`, `_iterative_process_action`, `_postprocess_action` - `fake_diffusion_cond` for buffer preallocation -- Optional `preprocess_observation` to cache expensive conditioning +- `_get_value`. It is not abstract, but the base assigns its result into + `runtime_state.value`, so a subclass that leaves it out fails mid-rollout with + `TypeError: can't assign a NoneType to a torch.FloatTensor`. +- Optional `build_obs_cache` to cache expensive conditioning. The base calls it + once per inference and passes the result to `_denoising_step` as the + keyword-only `cond_cache=` and to `_get_value` as `obs_cache=`. -Key shapes (from `fake_internal_state`). +Key shapes (from `fake_runtime_state`). - `action/logprob/entropy/obs["x"]`: `(B, S, H, D)` - `obs["t"]`: `(B, S)` @@ -99,12 +104,15 @@ Minimal template. ```py import dataclasses +from typing import Any + +import numpy as np import torch -from tensordict import TensorDict from plugrl_server.policy.base_policy_gradient_diffusion_policy import ( BasePolicyGradientDiffusionPolicy, BasePolicyGradientDiffusionPolicyConfig, + TorchTree, ) from plugrl_server.policy.registration import register_policy, register_policy_config @@ -123,23 +131,23 @@ class MyDPPOPolicy(BasePolicyGradientDiffusionPolicy): super().__init__(config) ... - def prepare_observation(self, obs: dict) -> TensorDict: + def prepare_observation(self, _obs: dict) -> dict[str, np.ndarray]: ... def _get_timesteps(self) -> torch.Tensor: ... - def _initialize_x(self, obs: TensorDict) -> torch.Tensor: + def _initialize_x(self, batch_size: int) -> torch.Tensor: ... def _denoising_step( self, x: torch.Tensor, t: torch.Tensor, - cond: TensorDict, + cond: TorchTree, x_next: torch.Tensor | None = None, *, - processed_cond=None, + cond_cache: Any = None, sampling_noise_level: float | None = None, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: ... @@ -147,10 +155,13 @@ class MyDPPOPolicy(BasePolicyGradientDiffusionPolicy): def _iterative_process_action(self, action: torch.Tensor) -> torch.Tensor: return action - def _postprocess_action(self, action: torch.Tensor, obs: TensorDict): + def _postprocess_action(self, action: torch.Tensor, obs: TorchTree) -> Any: + ... + + def _get_value(self, obs: TorchTree, obs_cache: Any = None) -> torch.Tensor: ... - def fake_diffusion_cond(self, batch_size: int) -> TensorDict: + def fake_diffusion_cond(self, batch_size: int) -> TorchTree: ... ``` @@ -160,8 +171,9 @@ See `plugrl-server/examples/lerobot/lerobot_diffusion.py` (`UID = "lerobot-diffu Patterns to copy. -- Pack multi-step observations into a batched `TensorDict`. -- Cache encoder outputs in `preprocess_observation`. +- Pack multi-step observations into one batched nested `dict` of arrays - that is + what `TorchTree` is; the base converts it to tensors for you. +- Cache encoder outputs in `build_obs_cache`. - Support expanded batch `B * num_denoising_steps` with `repeat_interleave`. - Deterministic sampling can return zero `logprob` like `Pi0Policy`. - Stochastic sampling should compute `logprob/entropy` like `DPPOPolicy`. @@ -170,7 +182,7 @@ Patterns to copy. - `dppo-policy` import fails: install `dppo` in the environment that runs `plugrl-run-server`. - `pi0-policy` import/setup fails: follow the OpenPI setup in the server repo. -- Shape mismatch in training: keep action shapes stable and align `InternalState` with your buffers. +- Shape mismatch in training: keep action shapes stable and align the runtime state with your buffers. - Policy UID not listed: your registration module was not imported. ## Next steps diff --git a/docs/policy/dppo_policy.zh.md b/docs/policy/dppo_policy.zh.md index 708ad4f..453f72e 100644 --- a/docs/policy/dppo_policy.zh.md +++ b/docs/policy/dppo_policy.zh.md @@ -78,18 +78,23 @@ python -c "import my_pkg.plugrl_policies; from plugrl_server.cli import main; ma 继承 `BasePolicyGradientDiffusionPolicy`: `plugrl-server/src/plugrl_server/policy/base_policy_gradient_diffusion_policy.py`。 -基类负责 denoising 循环并填充 `InternalState`: +基类负责 denoising 循环并填充 `DiffusionRuntimeState`: - 每步写入:`action`、`logprob`、`entropy`,以及 `obs["x"]`、`obs["t"]`。 -- 最终调用 `_postprocess_action`,并把 `value` 写入 `internal_state.value`。 +- 最终调用 `_postprocess_action`,并把 `value` 写入 `runtime_state.value`。 你需要实现。 - `_get_timesteps`、`_initialize_x`、`_denoising_step`、`_iterative_process_action`、`_postprocess_action` - `fake_diffusion_cond`(用于 buffer 预分配) -- 可选 `preprocess_observation`(缓存昂贵的条件编码) +- `_get_value`。它不是 abstract,但基类会把它的返回值写进 `runtime_state.value`, + 所以不实现它会在 rollout 中途报 + `TypeError: can't assign a NoneType to a torch.FloatTensor`。 +- 可选 `build_obs_cache`(缓存昂贵的条件编码)。基类每次推理调用它一次, + 结果以 keyword-only 的 `cond_cache=` 传给 `_denoising_step`, + 以 `obs_cache=` 传给 `_get_value`。 -关键形状(来自 `fake_internal_state`)。 +关键形状(来自 `fake_runtime_state`)。 - `action/logprob/entropy/obs["x"]`:`(B, S, H, D)` - `obs["t"]`:`(B, S)` @@ -99,12 +104,15 @@ python -c "import my_pkg.plugrl_policies; from plugrl_server.cli import main; ma ```py import dataclasses +from typing import Any + +import numpy as np import torch -from tensordict import TensorDict from plugrl_server.policy.base_policy_gradient_diffusion_policy import ( BasePolicyGradientDiffusionPolicy, BasePolicyGradientDiffusionPolicyConfig, + TorchTree, ) from plugrl_server.policy.registration import register_policy, register_policy_config @@ -123,23 +131,23 @@ class MyDPPOPolicy(BasePolicyGradientDiffusionPolicy): super().__init__(config) ... - def prepare_observation(self, obs: dict) -> TensorDict: + def prepare_observation(self, _obs: dict) -> dict[str, np.ndarray]: ... def _get_timesteps(self) -> torch.Tensor: ... - def _initialize_x(self, obs: TensorDict) -> torch.Tensor: + def _initialize_x(self, batch_size: int) -> torch.Tensor: ... def _denoising_step( self, x: torch.Tensor, t: torch.Tensor, - cond: TensorDict, + cond: TorchTree, x_next: torch.Tensor | None = None, *, - processed_cond=None, + cond_cache: Any = None, sampling_noise_level: float | None = None, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: ... @@ -147,10 +155,13 @@ class MyDPPOPolicy(BasePolicyGradientDiffusionPolicy): def _iterative_process_action(self, action: torch.Tensor) -> torch.Tensor: return action - def _postprocess_action(self, action: torch.Tensor, obs: TensorDict): + def _postprocess_action(self, action: torch.Tensor, obs: TorchTree) -> Any: + ... + + def _get_value(self, obs: TorchTree, obs_cache: Any = None) -> torch.Tensor: ... - def fake_diffusion_cond(self, batch_size: int) -> TensorDict: + def fake_diffusion_cond(self, batch_size: int) -> TorchTree: ... ``` @@ -160,8 +171,8 @@ class MyDPPOPolicy(BasePolicyGradientDiffusionPolicy): 可复用模式。 -- 把多步观测打包成 batched `TensorDict`。 -- 在 `preprocess_observation` 缓存 encoder 输出。 +- 把多步观测打包成一个 batched 的嵌套 `dict`(这就是 `TorchTree`),基类会替你转成张量。 +- 在 `build_obs_cache` 缓存 encoder 输出。 - 用 `repeat_interleave` 支持 `B * num_denoising_steps` 的扩展 batch。 - 确定性采样可像 `Pi0Policy` 一样返回全 0 的 `logprob`。 - 随机采样按分布计算 `logprob/entropy`,与 `DPPOPolicy` 对齐。 @@ -170,7 +181,7 @@ class MyDPPOPolicy(BasePolicyGradientDiffusionPolicy): - `dppo-policy` import 失败:在运行 `plugrl-run-server` 的环境里安装 `dppo`。 - `pi0-policy` 启动失败:按 OpenPI README 完成本地设置。 -- 训练 shape 对不上:动作形状要稳定,`InternalState` 字段要与 buffer 对齐。 +- 训练 shape 对不上:动作形状要稳定,runtime state 字段要与 buffer 对齐。 - CLI 找不到 UID:注册模块没有被 import。 ## 下一步 diff --git a/docs/policy/index.md b/docs/policy/index.md index e8037be..6f76055 100644 --- a/docs/policy/index.md +++ b/docs/policy/index.md @@ -21,6 +21,7 @@ plugrl-run-server --help - `dummy-policy`: random actions for protocol smoke tests - `dppo-policy`: DPPO diffusion policy +- `fpo-policy`: FPO flow-matching policy, used by the get-started run - `pi0-policy`: OpenPI policy, requires a checkpoint path OpenPI example. diff --git a/docs/policy/index.zh.md b/docs/policy/index.zh.md index e6cbf71..a4bf2a1 100644 --- a/docs/policy/index.zh.md +++ b/docs/policy/index.zh.md @@ -21,6 +21,7 @@ plugrl-run-server --help - `dummy-policy`:随机动作,用于协议联通性验证 - `dppo-policy`:DPPO diffusion 策略 +- `fpo-policy`:FPO flow matching 策略,快速上手那条命令用的就是它 - `pi0-policy`:OpenPI 策略,需要 checkpoint 路径 OpenPI 示例。 diff --git a/docs/protocol/index.md b/docs/protocol/index.md index 7ce09dd..b0553af 100644 --- a/docs/protocol/index.md +++ b/docs/protocol/index.md @@ -68,11 +68,27 @@ happens, which is what makes it worth stating. ## Checking an implementation `plugrl-protocol` ships a server that grades a client against the -specification clause by clause and exits non-zero on a violation: +specification clause by clause and exits non-zero on a violation. + +From a fresh checkout of `plugrl-protocol`, two things are not already in place. +The conformance server imports `websockets`, which is not a declared dependency - +`pyproject.toml` lists only `numpy` and `msgpack`, so `uv sync` leaves it out. And +the C++ client is a source file, not a binary, so it has to be compiled first. +Both steps are what CI does: + +```bash +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` uses POSIX sockets (`sys/socket.h`, `arpa/inet.h`), so that +build needs Linux, macOS or WSL. The Python reference client has no such +constraint and exercises the same clauses: ```bash -python examples/conformance_server.py --port 8000 --steps 20 & -./plugrl_client 127.0.0.1 8000 20 +uv run --with websockets examples/raw_client.py --host 127.0.0.1 --port 8000 --steps 20 ``` Its report has two severities. A **violation** is something the real server diff --git a/docs/protocol/index.zh.md b/docs/protocol/index.zh.md index 58f7dc4..58523c9 100644 --- a/docs/protocol/index.zh.md +++ b/docs/protocol/index.zh.md @@ -56,11 +56,25 @@ step state、终止标志 —— 只活在一条连接里。重连的客户端 ## 检验一个实现 -`plugrl-protocol` 附带一个服务端,它按规范逐条给客户端打分,有违规就以非零码退出: +`plugrl-protocol` 附带一个服务端,它按规范逐条给客户端打分,有违规就以非零码退出。 + +刚 clone 下来的 `plugrl-protocol` 还差两步。conformance server 会 import +`websockets`,但它不是声明的依赖 —— `pyproject.toml` 里只有 `numpy` 和 `msgpack`, +`uv sync` 装不上它;另外 C++ 客户端是源码不是可执行文件,要先编译。CI 做的就是这两步: + +```bash +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.h`、`arpa/inet.h`), +所以这一步需要 Linux、macOS 或 WSL。Python 参考客户端没有这个限制, +覆盖的条款是同一批: ```bash -python examples/conformance_server.py --port 8000 --steps 20 & -./plugrl_client 127.0.0.1 8000 20 +uv run --with websockets examples/raw_client.py --host 127.0.0.1 --port 8000 --steps 20 ``` 报告分两个等级。**violation** 是真服务端会拒绝或处理错的问题;**note** 是真服务端 diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index 014b78e..0549ac8 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -5,8 +5,10 @@ Run PlugRL end to end: start a server, then start one or more env clients. ## Quickstart ```bash -plugrl-run-server fpo-policy default fpo default \n --policy.device cpu --algo.global-steps 500000 --algo.buffer-size 4096 -plugrl-run-env-client mujoco-v1 --server-host 127.0.0.1 --server-port 8000 \n --num-envs 1 --num-episodes 600 --runner.replan-steps 1 --runner.seed 0 +plugrl-run-server fpo-policy default fpo default \ + --policy.device cpu --algo.global-steps 500000 --algo.buffer-size 4096 +plugrl-run-env-client mujoco-v1 --server-host 127.0.0.1 --server-port 8000 \ + --num-envs 1 --num-episodes 600 --runner.replan-steps 1 --runner.seed 0 ``` That pair learns. [Get Started](get_started.md) explains the two flags that diff --git a/docs/user_guide/index.zh.md b/docs/user_guide/index.zh.md index d81b2b0..2cf2506 100644 --- a/docs/user_guide/index.zh.md +++ b/docs/user_guide/index.zh.md @@ -5,8 +5,10 @@ ## 快速开始 ```bash -plugrl-run-server fpo-policy default fpo default \n --policy.device cpu --algo.global-steps 500000 --algo.buffer-size 4096 -plugrl-run-env-client mujoco-v1 --server-host 127.0.0.1 --server-port 8000 \n --num-envs 1 --num-episodes 600 --runner.replan-steps 1 --runner.seed 0 +plugrl-run-server fpo-policy default fpo default \ + --policy.device cpu --algo.global-steps 500000 --algo.buffer-size 4096 +plugrl-run-env-client mujoco-v1 --server-host 127.0.0.1 --server-port 8000 \ + --num-envs 1 --num-episodes 600 --runner.replan-steps 1 --runner.seed 0 ``` 这一对**真的会学**。[快速开始](get_started.zh.md)里说明了那两个不可省的参数,