Source code for trinity.common.models.allocator

"""Allocator module for managing inference engines."""

import asyncio
import os
from collections import defaultdict
from copy import deepcopy
from dataclasses import dataclass
from typing import Dict, List, Tuple

import ray
from ray.util.placement_group import (
    PlacementGroup,
    placement_group,
    placement_group_table,
)
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy

from trinity.common.config import ExplorerConfig, InferenceModelConfig
from trinity.common.models.model import ModelWrapper
from trinity.utils.device import get_ray_resource_key, is_npu
from trinity.utils.log import get_logger


[docs] @dataclass class BundleResult: """Data class for storing the result of bundle allocation.""" bundles: List[Dict[str, float]] actor_bundle_map: Dict[str, int] bundle_actor_map: Dict[int, str]
[docs] class Allocator: """Allocator class for managing inference engines."""
[docs] def __init__(self, config: ExplorerConfig): """Initialize the Allocator.""" self.config = config self.logger = get_logger(__name__)
[docs] def get_actor_name(self, role: str, engine_id: int, node_id: int) -> str: """Generate a unique actor name based on the model config, engine ID, and node ID.""" return f"{self.config.name}_{role}_model_{engine_id}_{node_id}"
@property def placement_group_name(self) -> str: """Generate a descriptive name for the inference model placement group.""" return f"{self.config.name}_rollout_model"
[docs] def allocate_bundles(self) -> BundleResult: """Allocate bundles for the rollout model and auxiliary models based on the configuration.""" rollout_model = self.config.rollout_model auxiliary_models = self.config.auxiliary_models model_configs = [("rollout", rollout_model)] + [ (f"auxiliary_{index}", model) for index, model in enumerate(auxiliary_models) ] bundles: List[Dict[str, float]] = [] actor_bundle_map: Dict[str, int] = {} bundle_actor_map: Dict[int, str] = {} bundle_id = 0 for role, config in model_configs: gpus_per_bundle = config.gpu_per_engine // config.nnodes for engine_id in range(config.engine_num): for node_id in range(config.nnodes): bundles.append({get_ray_resource_key(): float(gpus_per_bundle), "CPU": 1}) actor_name = self.get_actor_name(role, engine_id, node_id) actor_bundle_map[actor_name] = bundle_id bundle_actor_map[bundle_id] = actor_name bundle_id += 1 return BundleResult( bundles=bundles, actor_bundle_map=actor_bundle_map, bundle_actor_map=bundle_actor_map )
[docs] def analyze_placement_group(self, pg: PlacementGroup, bundle_result: BundleResult): bundle_node_map = placement_group_table(pg)["bundles_to_node_id"] node_bundle_map = defaultdict(list) for bundle_id, node_id in bundle_node_map.items(): node_bundle_map[node_id].append(bundle_id) for node_id, bundle_ids in node_bundle_map.items(): self.logger.info("Node %s bundles:", node_id) for bundle_id in bundle_ids: actor_name = bundle_result.bundle_actor_map[bundle_id] self.logger.info(" > Bundle %s: Actor %s", bundle_id, actor_name)
[docs] async def create_engine( self, config: InferenceModelConfig, role: str, engine_id: int ) -> ModelWrapper: config = deepcopy(config) config.engine_id = engine_id actor_bundle_lists = [] for node_id in range(config.nnodes): actor_name = self.get_actor_name(role, engine_id, node_id) actor_bundle_lists.append((actor_name, self.bundle_result.actor_bundle_map[actor_name])) model_cls = None if config.engine_type.startswith("vllm"): from trinity.common.models.vllm_model import vLLMRolloutModel model_cls = vLLMRolloutModel elif config.engine_type == "sglang": from trinity.common.models.sglang_model import SGLangRolloutModel model_cls = SGLangRolloutModel elif config.engine_type == "tinker": from trinity.common.models.tinker_model import TinkerModel model_cls = TinkerModel elif config.engine_type == "external": return await get_external_model_wrapper(config=config) else: raise ValueError(f"Unsupported engine type: {config.engine_type}") self.logger.info( f"Creating inference_model {self.get_actor_name(role, engine_id, 0)} in {config.ray_namespace}." ) return await get_model_wrapper(model_cls, config, self.pg, actor_bundle_lists)
[docs] async def create_all_models(self) -> Tuple[List[ModelWrapper], List[List[ModelWrapper]]]: """Create all model actors for the rollout model and auxiliary models based on the configuration.""" if self.config.mode == "colocate": return await self._create_colocate_model() self.bundle_result = self.allocate_bundles() self.pg = placement_group( self.bundle_result.bundles, strategy="PACK", name=self.placement_group_name, ) await self.pg.ready() self.analyze_placement_group(self.pg, self.bundle_result) # create rollout_models tasks = [] for engine_id in range(self.config.rollout_model.engine_num): tasks.append( asyncio.create_task( self.create_engine(self.config.rollout_model, "rollout", engine_id) ) ) # create auxiliary models for index, auxiliary_model_config in enumerate(self.config.auxiliary_models): for engine_id in range(auxiliary_model_config.engine_num): tasks.append( asyncio.create_task( self.create_engine(auxiliary_model_config, f"auxiliary_{index}", engine_id) ) ) # wait for all models to be created results = await asyncio.gather(*tasks) rollout_models: List[ModelWrapper] = results[: self.config.rollout_model.engine_num] auxiliary_models: List[List[ModelWrapper]] = [ results[ self.config.rollout_model.engine_num + sum( self.config.auxiliary_models[i].engine_num for i in range(index) ) : self.config.rollout_model.engine_num + sum(self.config.auxiliary_models[i].engine_num for i in range(index + 1)) ] for index in range(len(self.config.auxiliary_models)) ] return rollout_models, auxiliary_models
async def _create_colocate_model( self, ) -> Tuple[List[ModelWrapper], List[List[ModelWrapper]]]: """Create the rollout model without reserving a placement group. In colocate mode the trainer and rollout model share the same GPU. Reserving that GPU in a placement group would prevent the trainer from acquiring it. """ config = deepcopy(self.config.rollout_model) config.engine_id = 0 actor_name = self.get_actor_name("rollout", 0, 0) config.ray_actor_name = actor_name if not config.engine_type.startswith("vllm"): raise ValueError( f"Colocate mode only supports vLLM, but got engine type: {config.engine_type}" ) from trinity.common.models.vllm_model import vLLMRolloutModel self.logger.info( "Creating colocated inference_model %s in %s.", actor_name, config.ray_namespace, ) handler = ( ray.remote(vLLMRolloutModel) .options( name=actor_name, num_cpus=0, num_gpus=0, namespace=config.ray_namespace, ) .remote(config=config) ) await handler.prepare.remote() server_address = await handler.get_api_server_url.remote() wrapper = ModelWrapper(models=[handler], config=config, api_address=server_address) await wrapper.prepare() return [wrapper], []
[docs] def get_model(self, config: InferenceModelConfig, role: str, engine_id: int) -> ModelWrapper: """Get the model actor for the given role and engine ID.""" actor_name = self.get_actor_name(role, engine_id, 0) try: model_actor = ray.get_actor(actor_name, namespace=config.ray_namespace) return ModelWrapper(model=model_actor, config=config) except ValueError: self.logger.error( "Actor %s not found in %s. Make sure the model is created.", actor_name, config.ray_namespace, ) raise
[docs] async def get_model_wrapper( actor_cls, config: "InferenceModelConfig", pg: PlacementGroup, actor_bundle_list: List[Tuple[str, int]], ) -> ModelWrapper: """Get the Ray actor wrapper for the inference model. Creates Ray actors with appropriate GPU allocation and distributed communication setup based on the inferred launch mode. Args: actor_cls: The actor class to instantiate (e.g., vLLMRolloutModel, SGLangRolloutModel). config (InferenceModelConfig): The model config. pg (PlacementGroup): The placement group for the actors. actor_bundle_list (List[Tuple[str, int]]): The list of (actor_name, bundle_id) tuples for distributed setup. Returns: ModelWrapper: A wrapper for the model actors with distributed communication setup. """ handlers = [] for i, (actor_name, bundle_id) in enumerate(actor_bundle_list): engine_config = deepcopy(config) engine_config.ray_actor_name = actor_name engine_config.node_rank = i if is_npu(): handlers.append( ray.remote(actor_cls) .options( name=actor_name, resources={"NPU": engine_config.gpu_per_engine / engine_config.nnodes}, namespace=engine_config.ray_namespace, scheduling_strategy=PlacementGroupSchedulingStrategy( placement_group=pg, placement_group_capture_child_tasks=True, placement_group_bundle_index=bundle_id, ), ) .remote(config=engine_config) ) else: handlers.append( ray.remote(actor_cls) .options( name=actor_name, num_gpus=engine_config.gpu_per_engine / engine_config.nnodes, namespace=engine_config.ray_namespace, scheduling_strategy=PlacementGroupSchedulingStrategy( placement_group=pg, placement_group_capture_child_tasks=True, placement_group_bundle_index=bundle_id, ), ) .remote(config=engine_config) ) if len(actor_bundle_list) > 1: # get master address and port from the first handler and set it to all handlers for distributed communication master_addr, master_port = await handlers[0].get_available_address.remote(random_port=True) for handler in handlers: await handler.set_master_addr_port.remote(master_addr, master_port) await asyncio.gather(*[handler.prepare.remote() for handler in handlers]) server_address = await handlers[0].get_api_server_url.remote() wrapper = ModelWrapper(models=handlers, config=config, api_address=server_address) await wrapper.prepare() return wrapper
[docs] async def get_external_model_wrapper(config: InferenceModelConfig) -> ModelWrapper: """Get the ModelWrapper for the external model.""" if config.engine_type != "external": raise ValueError("This function is only for creating external model wrapper.") api_address = os.environ.get(config.external_model_config.base_url_env) api_address = api_address.rstrip("/") if api_address else None api_key = os.environ.get(config.external_model_config.api_key_env, "EMPTY") config.api_key = api_key config.enable_openai_api = True return ModelWrapper(models=[], config=config, api_address=api_address)