Source code for rlightning.policy.policy_group

"""Policy group module for managing multiple policy workers.

This module provides the PolicyGroup class for coordinating multiple
train and eval policies, including weight distribution, communication
group initialization, and rollout management.
"""

import time
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
from typing import Any, Dict, List, Optional

import ray
from ray.actor import ActorHandle

from rlightning.env import EnvMeta
from rlightning.types import BatchedData
from rlightning.utils.config import PolicyConfig, TrainConfig
from rlightning.utils.distributed.group_initializer import ParallelMode
from rlightning.utils.logger import get_logger
from rlightning.utils.placement import get_global_resource_manager
from rlightning.utils.ray import TaskSubmitter, resolve_object
from rlightning.utils.utils import InternalFlag
from rlightning.weights.utils import build_weight_buffer

from .base_policy import BasePolicy, PolicyRole
from .utils.router import SyncNodeAffinityRouter, create_router

logger = get_logger(__name__)


[docs] class PolicyGroup: """Manager for collections of train and eval policies. Coordinates multiple policies for distributed training and evaluation, handling weight distribution, communication groups, and rollout batching. """ def __init__( self, train_policy_list: List[BasePolicy | ActorHandle], eval_policy_list: List[BasePolicy | ActorHandle], config: PolicyConfig, ) -> None: """Create policy group with given training policy (actor) list and eva policy (actor) list. Args: train_policy_list (List[BasePolicy | ActorHandle]): A list of training policy (actor) eval_policy_list (List[BasePolicy | ActorHandle]): A list of policy (actor) for evalutation config (PolicyConfig): Policy configuration instance. """ self.config = config self.policy_list: Dict[PolicyRole, List[BasePolicy | ActorHandle]] = { PolicyRole.TRAIN: train_policy_list, PolicyRole.EVAL: eval_policy_list, } self.rollout_mode = config.rollout_mode self.placement_info: Dict[str, Dict[str, List]] = {} self.weight_buffer_strategy: Optional[str] = None self.transfer_info: Dict[str, List[Dict]] = {} self.router_map: Dict[str, List[str]] = {} self.shared_weight_buffer_map: Dict[str, Dict] = {} self._global_resource_manager = get_global_resource_manager() # Eval submitter allows pipelining _rollout and _postprocess on the same actor self._eval_task_submitter = TaskSubmitter(max_pending_tasks_each_worker=8) # Train submitter keeps max_pending=1 to guarantee ordering self._train_task_submitter = TaskSubmitter(max_pending_tasks_each_worker=1) self.router_type = config.router_type self.router = create_router( rollout_mode=self.rollout_mode, router_type=self.router_type, eval_policies=self.eval_list, global_resource_manager=self._global_resource_manager, ) logger.info(f"[PolicyGroup] Using router: {type(self.router).__name__}") # Pre-initialized thread pool for node_affinity router to avoid per-call overhead self._rollout_executor: Optional[ThreadPoolExecutor] = None if isinstance(self.router, SyncNodeAffinityRouter): num_workers = self.router.env_worker_num self._rollout_executor = ThreadPoolExecutor(max_workers=num_workers) logger.info(f"[PolicyGroup] Initialized rollout executor with {num_workers} workers") def _submitter_for(self, policy) -> TaskSubmitter: """Return the appropriate TaskSubmitter for a given policy.""" if policy in self.policy_list[PolicyRole.TRAIN]: return self._train_task_submitter return self._eval_task_submitter @property def eval_list(self) -> List[BasePolicy | ActorHandle]: """Return the list of policies for evaluation Returns: List of evaluation policy instances. """ return self.policy_list[PolicyRole.EVAL] @property def train_list(self) -> List[BasePolicy | ActorHandle]: """Return the list of policies for training Returns: List of training policy instances. """ return self.policy_list[PolicyRole.TRAIN]
[docs] def init( self, train_config: TrainConfig, env_meta: Optional[EnvMeta] = None, eval_config: Optional[Any] = None, ) -> None: """Initialize both training and evaluation policies.""" self.init_eval(eval_config, env_meta) self.init_train(train_config, env_meta)
[docs] def init_placement_info(self, is_colocated: bool = False) -> None: """Initialize the placement info of the policy group with the consideration of given policy config. `placement_info` is a dict that mapping from ray_node_id to a dict of training/evaluation policy (actor). """ # build transfer info if is_colocated: self.transfer_info = build_transfer_info_for_colocated(self.train_list, self.eval_list) return for policy in self.train_list: if InternalFlag.REMOTE_TRAIN: node_id = ray.get(policy._get_node_id.remote()) else: node_id = "local" if node_id not in self.placement_info: self.placement_info[node_id] = { "train_policies": [], "eval_policies": [], } self.placement_info[node_id]["train_policies"].append(policy) for policy in self.eval_list: if InternalFlag.REMOTE_EVAL: node_id = ray.get(policy._get_node_id.remote()) else: node_id = "local" if node_id not in self.placement_info: self.placement_info[node_id] = { "train_policies": [], "eval_policies": [], } self.placement_info[node_id]["eval_policies"].append(policy) logger.info(f"placement_info: {self.placement_info}") self.transfer_info = build_transfer_info_for_disaggregated( self.placement_info, self.config.weight_buffer.buffer_strategy )
[docs] def init_weight_buffer(self) -> None: """Initialize the weight buffer based on the configured strategy. The weights buffer is created by the buffer strategy: None: no weight buffer Double: each eval policy has its own weight buffer Shared: shared weight buffer between eval policies on the same node Sharded: not supported now Raises: ValueError: If the weight buffer strategy is unsupported. """ self.weight_buffer_strategy = self.config.weight_buffer.buffer_strategy if self.weight_buffer_strategy == "None": pass elif self.weight_buffer_strategy == "Double": for policy in self.eval_list: self._eval_task_submitter.submit(policy.init_weight_buffer, _block=True) elif self.weight_buffer_strategy == "Shared": self.shared_weight_buffer_map = {} for node_id, policies_info in self.placement_info.items(): if len(policies_info["eval_policies"]) == 0: continue shared_weight_buffer = build_weight_buffer( self.config.weight_buffer.type, self.config.weight_buffer, node_id=node_id, ) for policy in policies_info["eval_policies"]: self._eval_task_submitter.submit(policy.init_weight_buffer, shared_weight_buffer, _block=True) self.shared_weight_buffer_map[node_id] = { "shared_weight_buffer": shared_weight_buffer, "eval_policies": policies_info["eval_policies"], } else: raise ValueError(f"Unsupported weight buffer strategy: {self.weight_buffer_strategy}")
[docs] def init_comm_group(self, backend: str = "nccl", is_colocated: bool = False) -> None: """Initialize communication groups for distributed training. Sets up distributed environment, intra-node groups, weight transfer groups, and DDP communication groups. Args: backend: Communication backend ('nccl' or 'gloo'). is_colocated: If True, initialize only for training policies. """ if not self.train_list and not self.eval_list: return if is_colocated: all_policies = self.train_list else: all_policies = self.train_list + self.eval_list # init distributed environment actors_by_node = defaultdict(list) for policy in all_policies: node_id = self._submitter_for(policy).submit(policy._get_node_id, _block=True) actors_by_node[node_id].append(policy) # Sort actors by node and gpu id sorted_actors = [] local_rank_list = [] local_world_size_list = [] for node_id in actors_by_node: actors_by_node[node_id] = sorted( actors_by_node[node_id], key=lambda x: self._submitter_for(x).submit(x._get_gpu_ids, _block=True)[0], ) sorted_actors.extend(actors_by_node[node_id]) local_world_size = len(actors_by_node[node_id]) local_rank_list.extend(range(local_world_size)) local_world_size_list.extend([local_world_size] * local_world_size) # Choose master from rank-0 actor's node to match env:// requirement master_addr, master_port = ray.get(sorted_actors[0]._get_addr_and_port.remote()) world_size = len(sorted_actors) ref = [ policy.init_distributed_env.remote( rank=rank, world_size=world_size, local_rank=local_rank_list[rank], local_world_size=local_world_size_list[rank], master_addr=master_addr, master_port=master_port, backend=backend, ) for rank, policy in enumerate(sorted_actors) ] ray.get(ref) # init intra node communication group ref = [] for node_actors in actors_by_node.values(): intra_node_ranks = [ray.get(policy.get_rank.remote()) for policy in node_actors] ref.extend( [ policy.init_single_comm_group.remote(intra_node_ranks, ParallelMode.INTRA_NODE, backend) for policy in all_policies ] ) ray.get(ref) ray.get([policy.dist_barrier.remote() for policy in all_policies]) # init weight transfer group if self.weight_buffer_strategy != "None" and self.transfer_info: ref.clear() for node_id, transfer_info in self.transfer_info.items(): for transfer_info_item in transfer_info: if len(transfer_info_item["receiver"]) == 0: continue weight_transfer_ranks = [ ray.get(policy.get_rank.remote()) for policy in [transfer_info_item["sender"]] + transfer_info_item["receiver"] ] ref.extend( [ policy.init_single_comm_group.remote( weight_transfer_ranks, ParallelMode.WEIGHT_TRANSFER, backend, ) for policy in all_policies ] ) ray.get(ref) ray.get([policy.dist_barrier.remote() for policy in all_policies]) # if no train policy, only init eval policy if not self.train_list: return # init ddp communication group ddp_train_ranks = [ray.get(policy.get_rank.remote()) for policy in self.train_list] ref = [ policy.init_single_comm_group.remote(ddp_train_ranks, ParallelMode.TRAIN_DATA_PARALLEL, backend) for policy in all_policies ] ray.get(ref) ray.get([policy.dist_barrier.remote() for policy in all_policies])
[docs] def reset_training_state( self, train_config: TrainConfig, env_meta: Optional[Any] = None, seed: Optional[int] = None ) -> None: """Reset training policies after warm_up.""" # Submit all policy reset tasks in parallel (non-blocking) to avoid DDP deadlock refs = [ self._train_task_submitter.submit(policy.reset_training_state, train_config, env_meta, seed, _block=False) for policy in self.train_list ] # Wait for all tasks to complete (returns ObjectRef if Ray actor, otherwise direct result) if refs and isinstance(refs[0], ray.ObjectRef): ray.get(refs) # Synchronize only within train DDP group (don't use global barrier to avoid deadlock with eval ranks) ray.get([policy.dist_barrier.remote(ParallelMode.TRAIN_DATA_PARALLEL) for policy in self.train_list])
[docs] def print_timing_summary(self, reset: bool = False) -> None: """ Print the timing summary of the policy group. """ for policy in self.train_list + self.eval_list: self._submitter_for(policy).submit(policy.print_timing_summary, reset, _block=True)
[docs] def shutdown(self) -> None: """Shutdown the policy group and cleanup resources.""" if self._rollout_executor is not None: self._rollout_executor.shutdown(wait=False) self._rollout_executor = None
[docs] def rollout_batch(self, batched_env_ret: BatchedData) -> BatchedData: """Perform batched rollout across eval policies. Args: batched_env_ret: Batched environment returns. Returns: BatchedData containing policy responses. """ policy_resp_list = [] if self.rollout_mode == "async": # with load balancing, assign requests to the actor with the least load current_loads = ray.get([p.get_num_requests.remote() for p in self.eval_list]) assignments = self.router.assign(current_loads, len(batched_env_ret)) for env_ret, idx in zip(batched_env_ret.values(), assignments): policy = self.eval_list[idx] # use the asynchronous rollout method to enable concurrent requests on the actor policy_resp_list.append(policy._rollout_async.remote(env_ret)) else: env_ids = batched_env_ret.ids() env_rets = batched_env_ret.values() if isinstance(self.router, SyncNodeAffinityRouter): # For node_affinity router: use pre-initialized ThreadPoolExecutor to enable # concurrent scheduling across different nodes. The router uses per-node locks # internally, so max parallelism is limited by the number of nodes. def _submit_rollout(env_id: str, env_ret: Any) -> Any: policy = self.router.select_policy(env_id) return self._eval_task_submitter.submit(policy._rollout, env_ret) policy_resp_list = list(self._rollout_executor.map(_submit_rollout, env_ids, env_rets)) else: # For simple router: sequential execution is more efficient because # all selections are serialized by a single global lock anyway. # Using ThreadPoolExecutor would only add overhead without benefit. for env_id, env_ret in zip(env_ids, env_rets): policy = self.router.select_policy(env_id) policy_resp_list.append(self._eval_task_submitter.submit(policy._rollout, env_ret)) return BatchedData(batched_env_ret.ids(), policy_resp_list)
[docs] def postprocess( self, batched_env_ret: Optional[BatchedData] = None, batched_policy_resp: Optional[BatchedData] = None, ) -> BatchedData: """Submit postprocess tasks to eval policies. Args: batched_env_ret: Batched environment returns. batched_policy_resp: Batched policy responses. Returns: BatchedData containing processed results. """ batched_args = [] if batched_env_ret is not None: batched_args.append(batched_env_ret.values()) if batched_policy_resp is not None: batched_args.append(batched_policy_resp.values()) env_ids = batched_env_ret.ids() if batched_env_ret is not None else batched_policy_resp.ids() zipped_args = list(zip(*batched_args)) if isinstance(self.router, SyncNodeAffinityRouter) and self._rollout_executor is not None: # Parallel postprocess across nodes, mirroring rollout_batch's approach def _submit_postprocess(env_id: str, args: tuple) -> Any: policy = self.router.select_policy(env_id) return self._eval_task_submitter.submit(policy._postprocess, *args) processed_policy_resp_list = list( self._rollout_executor.map(_submit_postprocess, env_ids, zipped_args) ) else: # Sequential fallback for simple router processed_policy_resp_list = [] for env_id, args in zip(env_ids, zipped_args): policy = self.router.select_policy(env_id) processed_policy_resp_list.append(self._eval_task_submitter.submit(policy._postprocess, *args)) return BatchedData(env_ids, processed_policy_resp_list)
[docs] def update_dataset(self, sample_data: List) -> List[ray.ObjectRef]: """ Update the dataset in the policy group by getting sampled data from the buffer. Args: sample_data: List of sampled data objects, one per train policy. """ assert len(sample_data) == len(self.train_list) ret = [] for policy, data in zip(self.train_list, sample_data): ret.append(self._train_task_submitter.submit(policy.update_dataset, data)) return ret
[docs] def train(self, *args: Any, **kwargs: Any) -> Dict[str, float]: """Train all training policies. Args: *args: Variable positional arguments. **kwargs: Variable keyword arguments. Returns: Training info from the first policy. """ train_info = resolve_object( [self._train_task_submitter.submit(policy.train, *args, **kwargs) for policy in self.train_list] ) return train_info[0]
[docs] def push(self, policy_worker: BasePolicy | ActorHandle, role: PolicyRole) -> None: """Add new policy worker to existing group. Args: policy_worker (BasePolicy): Worker instance role (PolicyRole): Policy role, indicating training or evaluation. """ self.policy_list[role].append(policy_worker)
[docs] def pop(self, role: PolicyRole) -> BasePolicy | ActorHandle: """ Pop the last policy worker from the list. """ return self.policy_list[role].pop()
[docs] def convert_train_to_eval(self, num: int) -> None: """Convert training policies to evaluation policies. Args: num: Number of policies to convert. """ pass
[docs] def convert_eval_to_train(self, num: int) -> None: """Convert evaluation policies to training policies. Args: num: Number of policies to convert. """ pass
[docs] def notify_update_weights(self) -> None: """Signal all eval policies that new weights are available.""" for policy in self.eval_list: self._eval_task_submitter.submit(policy.notify_update_weights)
[docs] def wait_for_eval_weight_update(self, timeout: float = 60.0) -> None: """Block until every eval policy finishes its background weight update. No-op when ``weight_buffer_strategy == "None"`` because weights are transferred synchronously and no background thread is involved. Args: timeout: Per-policy wait timeout in seconds. """ refs = [ self._eval_task_submitter.submit(policy.wait_for_weight_update_done, timeout) for policy in self.eval_list ] if refs and isinstance(refs[0], ray.ObjectRef): results = ray.get(refs) for i, ok in enumerate(results): if not ok: logger.warning(f"[WeightConsistency] Eval policy {i} weight update timed out after {timeout}s")
[docs] def verify_eval_weight_consistency(self, rtol: float = 1e-3) -> bool: """Wait for eval weight updates and verify they match the first train policy. For each eval policy, per-parameter L2 norms are collected and compared against those of the first train policy. A relative tolerance *rtol* is applied: a mismatch is reported when |train_norm - eval_norm| / max(|train_norm|, 1e-8) > rtol Args: rtol: Relative tolerance for per-parameter norm comparison. Returns: ``True`` if every eval policy is consistent with the train policy, ``False`` if any mismatch is detected. """ if self.weight_buffer_strategy != "None": self.wait_for_eval_weight_update() ref_fingerprint: dict = self._train_task_submitter.submit(self.train_list[0].get_param_fingerprint, _block=True) all_consistent = True for i, policy in enumerate(self.eval_list): eval_fingerprint: dict = self._eval_task_submitter.submit(policy.get_param_fingerprint, _block=True) mismatches = [] for key, train_norm in ref_fingerprint.items(): if key not in eval_fingerprint: mismatches.append(f"{key}: missing in eval policy") continue eval_norm = eval_fingerprint[key] base = max(abs(train_norm), 1e-8) rel_diff = abs(train_norm - eval_norm) / base if rel_diff > rtol: mismatches.append( f"{key}: train_norm={train_norm:.6f}, " f"eval_norm={eval_norm:.6f}, rel_diff={rel_diff:.2e}" ) if mismatches: all_consistent = False logger.warning( f"[WeightConsistency] Eval policy {i} has {len(mismatches)} " f"mismatched parameter(s) (showing first 10):" ) for m in mismatches[:10]: logger.warning(f" {m}") else: logger.info(f"[WeightConsistency] Eval policy {i} weights match train policy.") return all_consistent
[docs] def save_checkpoint(self, path: str) -> None: """Save the checkpoint of train policy. If multiple train policies exist, only save the checkpoint of the first one. Args: path (str): The path to save the checkpoint. """ self._train_task_submitter.submit(self.train_list[0].save_checkpoint, path, _block=True)
[docs] def send_weights(self) -> None: """Broadcast weights from train to eval policies. Send weights from train to eval policies based on the configured weight buffer strategy. """ refs = [] _transfer_descriptions = [] for node_id, transfer_info in self.transfer_info.items(): for transfer_info_item in transfer_info: # train_policy is an actor of WeightTransferManager train_policy = transfer_info_item["sender"] n_receivers = len(transfer_info_item["receiver"]) if isinstance(transfer_info_item["receiver"], list) else 1 intra = transfer_info_item.get("intra_node", False) _transfer_descriptions.append(f"node={node_id},intra={intra},receivers={n_receivers}") if self.weight_buffer_strategy == "Shared" and transfer_info_item["intra_node"]: shared_weight_buffer = self.shared_weight_buffer_map[node_id]["shared_weight_buffer"] if InternalFlag.REMOTE_TRAIN: refs.append( train_policy.send_weights.remote(transfer_info_item["receiver"], shared_weight_buffer) ) else: train_policy.send_weights(transfer_info_item["receiver"], shared_weight_buffer) else: if InternalFlag.REMOTE_TRAIN: refs.append(train_policy.send_weights.remote(transfer_info_item["receiver"])) else: # for single process mode train_policy.send_weights(transfer_info_item["receiver"]) if len(refs) > 0: ray.get(refs)
[docs] def sync_weights(self): """ Broadcast the state dict to the eval policy and store into the weight_buffer for later loading. """ # (warining): may have bug when params is offload to cpu (in warm_up) if self.weight_buffer_strategy == "None": refs = [] for transfer_info_item in self.transfer_info["all"]: train_policy = transfer_info_item["sender"] eval_policy = transfer_info_item["receiver"] serialized_state_dict = train_policy.send_weights_ipc.remote() ref = self._eval_task_submitter.submit( eval_policy.recv_weights_ipc, serialized_state_dict ) refs.append(ref) resolve_object(refs) else: self.send_weights() self.notify_update_weights()
[docs] def init_eval(self, eval_config: Optional[Any] = None, env_meta: Optional[EnvMeta] = None) -> List: """Initialize all evaluation policies. Args: eval_config: Evaluation configuration. env_meta: Environment metadata. Returns: List of initialization results. """ resolve_object( [self._eval_task_submitter.submit(policy.init_eval, eval_config, env_meta) for policy in self.eval_list] )
[docs] def init_train(self, train_config: Any, env_meta: Optional[EnvMeta] = None) -> List: """Initialize all training policies. Args: train_config: Training configuration. env_meta: Environment metadata. Returns: List of initialization results. """ resolve_object( [self._train_task_submitter.submit(policy.init_train, train_config, env_meta) for policy in self.train_list] )
[docs] def offload_eval_model(self): """ Offload the eval model to cpu. """ resolve_object([self._eval_task_submitter.submit(policy.offload_model) for policy in self.eval_list])
[docs] def reload_eval_model(self): """ Reload the eval model to gpu. """ resolve_object([self._eval_task_submitter.submit(policy.reload_model) for policy in self.eval_list])
[docs] def offload_model_optimizer(self): """ Offload the model optimizer to cpu. """ resolve_object([self._train_task_submitter.submit(policy.offload_optimizer) for policy in self.train_list])
[docs] def offload_model_param_and_grad(self, offload_grad: bool = True, offload_optimizer: bool = True): """ Offload the model parameters and gradients to cpu. """ resolve_object( [ self._train_task_submitter.submit(policy.offload_model_param_and_grad, offload_grad=offload_grad) for policy in self.train_list ] ) if offload_optimizer: resolve_object([self._train_task_submitter.submit(policy.offload_optimizer) for policy in self.train_list])
[docs] def reload_model_param_and_grad(self, load_grad: bool = True, load_optimizer: bool = True): """ Reload the model parameters and gradients to gpu. """ resolve_object( [ self._train_task_submitter.submit(policy.reload_model_param_and_grad, load_grad=load_grad) for policy in self.train_list ] ) if load_optimizer: resolve_object([self._train_task_submitter.submit(policy.reload_optimizer) for policy in self.train_list])
[docs] def build_transfer_info_for_disaggregated( placement_info: Dict[str, Dict[str, List]], weight_buffer_strategy: str ) -> tuple[Dict[str, List[str]], Dict[str, List[Dict]]]: """Build weight transfer info for disaggregated.. Determines how weights should be transferred from train to eval policies based on their node placement and the weight buffer strategy. Args: placement_info: Dictionary mapping node IDs to policy lists. weight_buffer_strategy: Strategy for weight buffer management. Returns: A tuple containing: - router_map: Mapping from train nodes to receiver nodes. - transfer_info: Detailed transfer information per node. Raises: ValueError: If no train policy is found or unsupported strategy. """ # init transfer info router_map = {} transfer_info = {} node_info = {"train_nodes": [], "eval_nodes": [], "mixed_nodes": []} # classify nodes into train/eval/mixed nodes for node_id, policies_info in placement_info.items(): if len(policies_info["train_policies"]) == 0: node_info["eval_nodes"].append(node_id) elif len(policies_info["eval_policies"]) == 0: node_info["train_nodes"].append(node_id) else: node_info["mixed_nodes"].append(node_id) router_map[node_id] = [node_id] # process the router of the node which only has eval_policy if len(node_info["train_nodes"]) > 0: # if exist independent train_node,average assign eval_policy to train_node for idx, eval_node_id in enumerate(node_info["eval_nodes"]): train_node_id = node_info["train_nodes"][idx % len(node_info["train_nodes"])] if train_node_id not in router_map: router_map[train_node_id] = [] router_map[train_node_id].append(eval_node_id) elif len(node_info["mixed_nodes"]) > 0: # if not exist independent train_node,average assign eval_policy to mixed_node for idx, eval_node_id in enumerate(node_info["eval_nodes"]): mixed_node_id = node_info["mixed_nodes"][idx % len(node_info["mixed_nodes"])] router_map[mixed_node_id].append(eval_node_id) else: raise ValueError("No train policy found") for node_id, router_list in router_map.items(): train_policies = placement_info[node_id]["train_policies"] transfer_info[node_id] = [] for idx, router_node_id in enumerate(router_list): # average assign receiver_node to the train_policy of sender_node transfer_info_item = { "sender": train_policies[idx % len(train_policies)], "receiver": [], "intra_node": False, } if router_node_id == node_id: transfer_info_item["intra_node"] = True if weight_buffer_strategy in ["None", "Double"]: transfer_info_item["receiver"].extend(placement_info[router_node_id]["eval_policies"]) elif weight_buffer_strategy == "Shared": if router_node_id != node_id: transfer_info_item["receiver"].append(placement_info[router_node_id]["eval_policies"][0]) else: raise ValueError(f"Unsupported weight buffer strategy: {weight_buffer_strategy}") transfer_info[node_id].append(transfer_info_item) logger.debug(f"router_map: {router_map}") logger.debug(f"transfer_info: {transfer_info}") return transfer_info
[docs] def build_transfer_info_for_colocated(train_list: List[BasePolicy], eval_list: List[BasePolicy]): """ Build transfer info for colocation. Args: train_list: List of train policies. eval_list: List of eval policies. Returns: Transfer info for colocation. """ transfer_info_list = [] assert len(train_list) >= len(eval_list), "train_list must have more policies than eval_list for colocated mode" # here we assume the train_list and eval_list are in the same order for idx, policy in enumerate(eval_list): transfer_info_item = { "sender": train_list[idx], "receiver": policy, } transfer_info_list.append(transfer_info_item) return {"all": transfer_info_list}