Files
2026-07-13 13:17:40 +08:00

208 lines
8.4 KiB
Python

from typing import Any, Dict, List, Optional
import gymnasium as gym
from ray.rllib.connectors.connector_v2 import ConnectorV2
from ray.rllib.core import DEFAULT_MODULE_ID
from ray.rllib.core.columns import Columns
from ray.rllib.core.rl_module.multi_rl_module import MultiRLModule
from ray.rllib.core.rl_module.rl_module import RLModule
from ray.rllib.utils.annotations import override
from ray.rllib.utils.spaces.space_utils import BatchedNdArray, batch as batch_fn
from ray.rllib.utils.typing import EpisodeType
from ray.util.annotations import PublicAPI
@PublicAPI(stability="alpha")
class BatchIndividualItems(ConnectorV2):
"""Batches individual data-items (in lists) into tensors (with batch dimension).
Note: This is one of the default env-to-module or Learner ConnectorV2 pieces that
are added automatically by RLlib into every env-to-module/Learner connector
pipeline, unless `config.add_default_connectors_to_env_to_module_pipeline` or
`config.add_default_connectors_to_learner_pipeline ` are set to
False.
The default env-to-module connector pipeline is:
[
[0 or more user defined ConnectorV2 pieces],
AddObservationsFromEpisodesToBatch,
AddTimeDimToBatchAndZeroPad,
AddStatesFromEpisodesToBatch,
AgentToModuleMapping, # only in multi-agent setups!
BatchIndividualItems,
NumpyToTensor,
]
The default Learner connector pipeline is:
[
[0 or more user defined ConnectorV2 pieces],
AddObservationsFromEpisodesToBatch,
AddColumnsFromEpisodesToTrainBatch,
AddTimeDimToBatchAndZeroPad,
AddStatesFromEpisodesToBatch,
AgentToModuleMapping, # only in multi-agent setups!
BatchIndividualItems,
NumpyToTensor,
]
This ConnectorV2:
- Operates only on the input `data`, NOT the incoming list of episode objects
(ignored).
- In the single-agent case, `data` must already be a dict, structured as follows by
prior connector pieces of the same pipeline:
[col0] -> {[(eps_id,)]: [list of individual batch items]}
- In the multi-agent case, `data` must already be a dict, structured as follows by
prior connector pieces of the same pipeline (in particular the
`AgentToModuleMapping` piece):
[module_id] -> [col0] -> [list of individual batch items]
- Translates the above data under the different columns (e.g. "obs") into final
(batched) structures. For the single-agent case, the output `data` looks like this:
[col0] -> [possibly complex struct of batches (at the leafs)].
For the multi-agent case, the output `data` looks like this:
[module_id] -> [col0] -> [possibly complex struct of batches (at the leafs)].
.. testcode::
from ray.rllib.connectors.common import BatchIndividualItems
from ray.rllib.utils.test_utils import check
single_agent_batch = {
"obs": {
# Note that at this stage, next-obs is not part of the data anymore ..
("MA-EPS0",): [0, 1],
("MA-EPS1",): [2, 3],
},
"actions": {
# .. so we have as many actions per episode as we have observations.
("MA-EPS0",): [4, 5],
("MA-EPS1",): [6, 7],
},
}
# Create our (single-agent) connector piece.
connector = BatchIndividualItems()
# Call the connector (and thereby batch the individual items).
output_batch = connector(
rl_module=None, # This particular connector works without an RLModule.
batch=single_agent_batch,
episodes=[], # This particular connector works without a list of episodes.
explore=True,
shared_data={},
)
# `output_batch` should now be batched (episode IDs should have been removed
# from the struct).
check(
output_batch,
{"obs": [0, 1, 2, 3], "actions": [4, 5, 6, 7]},
)
"""
def __init__(
self,
input_observation_space: Optional[gym.Space] = None,
input_action_space: Optional[gym.Space] = None,
*,
multi_agent: bool = False,
**kwargs,
):
"""Initializes a BatchIndividualItems instance.
Args:
multi_agent: Whether this is a connector operating on a multi-agent
observation space mapping AgentIDs to individual agents' observations.
"""
super().__init__(
input_observation_space=input_observation_space,
input_action_space=input_action_space,
**kwargs,
)
self._multi_agent = multi_agent
@override(ConnectorV2)
def __call__(
self,
*,
rl_module: RLModule,
batch: Dict[str, Any],
episodes: List[EpisodeType],
explore: Optional[bool] = None,
shared_data: Optional[dict] = None,
**kwargs,
) -> Any:
is_multi_rl_module = isinstance(rl_module, MultiRLModule)
# Convert lists of individual items into properly batched data.
for column, column_data in batch.copy().items():
# Multi-agent case: This connector piece should only be used after(!)
# the AgentToModuleMapping connector has already been applied, leading
# to a batch structure of:
# [module_id] -> [col0] -> [list of individual batch items]
if is_multi_rl_module and column in rl_module:
# Case, in which a column has already been properly batched before this
# connector piece is called.
if not self._multi_agent:
continue
# If MA Off-Policy and independent sampling we need to overcome this
# check.
module_data = column_data
for col, col_data in module_data.copy().items():
if isinstance(col_data, list) and col != Columns.INFOS:
module_data[col] = batch_fn(
col_data,
individual_items_already_have_batch_dim="auto",
)
# Simple case: There is a list directly under `column`:
# Batch the list.
elif isinstance(column_data, list):
batch[column] = batch_fn(
column_data,
individual_items_already_have_batch_dim="auto",
)
# Single-agent case: There is a dict under `column` mapping
# `eps_id` to lists of items:
# Concat all these lists, then batch.
elif not self._multi_agent:
# TODO: only really need this in non-Learner connector pipeline
memorized_map_structure = []
list_to_be_batched = []
for (eps_id,) in column_data.keys():
items = column_data[(eps_id,)]
# Use extend instead of per-item append for better performance.
list_to_be_batched.extend(items)
# Only record structure for OBS column.
if column == Columns.OBS:
# Count total samples: BatchedNdArray items contribute
# len(item) samples, regular items contribute 1 each.
num_samples = sum(
len(item) if isinstance(item, BatchedNdArray) else 1
for item in items
)
memorized_map_structure.extend([eps_id] * num_samples)
# INFOS should not be batched (remain a list).
batch[column] = (
list_to_be_batched
if column == Columns.INFOS
else batch_fn(
list_to_be_batched,
individual_items_already_have_batch_dim="auto",
)
)
if is_multi_rl_module:
if DEFAULT_MODULE_ID not in batch:
batch[DEFAULT_MODULE_ID] = {}
batch[DEFAULT_MODULE_ID][column] = batch.pop(column)
# Only record structure for OBS column.
if column == Columns.OBS:
shared_data["memorized_map_structure"] = memorized_map_structure
# Multi-agent case: But Module ID not found in our RLModule -> Ignore this
# `module_id` entirely.
# else:
# pass
return batch