Files
ray-project--ray/rllib/env/env_runner_state_server.py
2026-07-13 13:17:40 +08:00

77 lines
3.6 KiB
Python

from typing import Optional
from ray.rllib.utils.metrics import WEIGHTS_SEQ_NO
from ray.rllib.utils.typing import StateDict
from ray.util.annotations import DeveloperAPI
@DeveloperAPI(stability="alpha")
class EnvRunnerStateServer:
"""A single, global actor holding the latest EnvRunner state for pull-based sync.
Used by async algorithms (IMPALA/APPO) when
``config.use_env_runner_state_server=True``: the Algorithm pushes the merged
EnvRunner state here once per weight sync, and every EnvRunner pulls it at the top
of each ``sample()`` call.
The state is the ``StateDict`` built by
:py:meth:`~ray.rllib.env.env_runner_group.EnvRunnerGroup.get_merged_env_runner_state`.
The RLModule weights are kept as a ``ray.ObjectRef`` (so an unchanged pull stays
cheap) and ``WEIGHTS_SEQ_NO`` is the version EnvRunners compare against.
Create with ``max_concurrency > 1`` so many EnvRunners can ``pull`` concurrently;
``push`` only rebinds the stored reference, so no lock is needed.
"""
# TODO(Artur): Target state (future PR): make this server the single source of truth
# for the *full* EnvRunner state (connectors + weights + counters), with the
# Algorithm holding a backup copy for server recreation. That collapses the two
# state-assembly paths (`get_merged_env_runner_state` + `sync_env_runner_states`)
# into one "build state" step plus a transport choice (sync algos push to workers,
# async pull from here), and lets us drop `_dont_auto_sync_env_runner_states` and
# the merge/broadcast config knobs. Keep merge on the driver; keep this server dumb.
def __init__(self):
self._state: Optional[StateDict] = None
def push(self, state: StateDict) -> None:
"""Stores the latest EnvRunner state (called once per weight sync).
Raises:
ValueError: If `state` carries no `WEIGHTS_SEQ_NO`. Such a state could never
be pulled (EnvRunners version-gate via `pull_if_newer`), so we reject it
here instead of silently holding state no EnvRunner would ever apply.
"""
if WEIGHTS_SEQ_NO not in state:
raise ValueError(
"Cannot push an EnvRunner state without a `WEIGHTS_SEQ_NO` (got keys: "
f"{list(state.keys())}). EnvRunners only apply a pulled state if its "
"version is newer than theirs, so a versionless state would be ignored "
"forever, rendering the server useless."
)
# Atomic rebind -> safe for concurrent `pull`s.
self._state = state
def pull(self) -> Optional[StateDict]:
"""Returns the latest stored state, or None if nothing has been pushed yet."""
return self._state
def pull_if_newer(self, weights_seq_no: int) -> Optional[StateDict]:
"""Returns the stored state, but only if it is newer than `weights_seq_no`.
Lets an EnvRunner do its freshness check in a single round-trip: the (heavy)
state dict crosses the wire only when there actually is a newer version;
otherwise this returns None and the caller keeps its current weights. Reads
``self._state`` exactly once, so it stays lock-free for concurrent pulls.
"""
state = self._state
if state is None or state.get(WEIGHTS_SEQ_NO, -1) <= weights_seq_no:
return None
return state
def get_version(self) -> int:
"""Returns the `WEIGHTS_SEQ_NO` of the stored state, or -1 if empty."""
if self._state is None:
return -1
return self._state.get(WEIGHTS_SEQ_NO, -1)