import logging import time from typing import TYPE_CHECKING, List import torch.cuda from sglang.srt.environ import envs from sglang.srt.eplb.expert_distribution import get_global_expert_distribution_recorder from sglang.srt.eplb.expert_location import ( ExpertLocationMetadata, format_expert_location_layout, format_expert_location_layout_diff, get_global_expert_location_metadata, ) if TYPE_CHECKING: from sglang.srt.model_executor.model_runner import ModelRunner logger = logging.getLogger(__name__) class EPLBManager: def __init__(self, model_runner: "ModelRunner"): super().__init__() self._model_runner = model_runner self._server_args = model_runner.server_args self._rebalance_layers_per_chunk = ( self._server_args.eplb_rebalance_layers_per_chunk ) self._rebalance_num_iterations = self._server_args.eplb_rebalance_num_iterations # Otherwise, the circular buffer will contain stale data. If the case is needed, it can be implemented. assert ( self._server_args.eplb_rebalance_num_iterations >= self._server_args.expert_distribution_recorder_buffer_size ), "eplb_rebalance_num_iterations must be greater than expert_distribution_recorder_buffer_size" if not get_global_expert_distribution_recorder().recording: get_global_expert_distribution_recorder().start_record() logger.info( f"[EPLBManager] system started, will rebalance per {self._rebalance_num_iterations} iterations." ) self._main_generator = self._entrypoint() def on_forward_pass_end(self): next(self._main_generator) def reset_generator(self): self._main_generator = self._entrypoint() # can be more complex if needed def _entrypoint(self): while True: for _ in range(self._rebalance_num_iterations): yield yield from self.rebalance() def rebalance(self): logger.info("[EPLBManager] rebalance start") enable_timing = self._rebalance_layers_per_chunk is None if enable_timing: torch.get_device_module().synchronize() time_start = time.time() dump_record_output = get_global_expert_distribution_recorder().dump_record( output_mode="object" ) logical_count = dump_record_output["logical_count"] average_utilization_rate_over_window = dump_record_output[ "average_utilization_rate_over_window" ] # Check whether rebalancing is needed if not self._check_rebalance_needed(average_utilization_rate_over_window): return expert_location_metadata = ExpertLocationMetadata.init_by_eplb( self._server_args, self._model_runner.model_config, logical_count ) update_layer_ids_chunks = self._compute_update_layer_ids_chunks() all_update_layer_ids = [ layer_id for chunk in update_layer_ids_chunks for layer_id in chunk ] self._log_rebalance_layout_before_update( expert_location_metadata, update_layer_ids=all_update_layer_ids, ) for chunk_layer_ids in update_layer_ids_chunks: if len(update_layer_ids_chunks) > 1: yield self._model_runner.update_expert_location( expert_location_metadata, update_layer_ids=chunk_layer_ids, ) self._log_rebalance_layout_after_update(update_layer_ids=all_update_layer_ids) msg = f"[EPLBManager] rebalance end" if enable_timing: torch.get_device_module().synchronize() time_end = time.time() msg += f" time={time_end - time_start:.3f}s" logger.info(msg) def _check_rebalance_needed(self, average_utilization_rate_over_window): if average_utilization_rate_over_window is None: return True if ( average_utilization_rate_over_window > self._server_args.eplb_min_rebalancing_utilization_threshold ): logger.info( f"[EPLBManager] Skipped ep rebalancing: current GPU utilization {average_utilization_rate_over_window:.2f} > minimum rebalance threshold {self._server_args.eplb_min_rebalancing_utilization_threshold:.2f}" ) return False return True def _compute_update_layer_ids_chunks(self) -> List[List[int]]: all_layer_ids = sorted( list(self._model_runner.model.routed_experts_weights_of_layer.keys()) ) chunk_size = self._rebalance_layers_per_chunk or 1000000 return list(_chunk_list(all_layer_ids, chunk_size=chunk_size)) def _should_log_expert_location_metadata(self) -> bool: return ( self._model_runner.tp_rank == 0 and envs.SGLANG_LOG_EXPERT_LOCATION_METADATA.get() ) def _log_rebalance_layout_before_update( self, new_expert_location_metadata: ExpertLocationMetadata, update_layer_ids: List[int], ): if not self._should_log_expert_location_metadata(): return old_expert_location_metadata = get_global_expert_location_metadata() logger.info( "[EPLBManager] rebalance layout before:\n%s", format_expert_location_layout( old_expert_location_metadata, layer_ids=update_layer_ids, ), ) logger.info( "[EPLBManager] rebalance layout target:\n%s", format_expert_location_layout( new_expert_location_metadata, layer_ids=update_layer_ids, ), ) logger.info( "[EPLBManager] rebalance layout diff:\n%s", format_expert_location_layout_diff( old_expert_location_metadata, new_expert_location_metadata, layer_ids=update_layer_ids, ), ) def _log_rebalance_layout_after_update(self, update_layer_ids: List[int]): if not self._should_log_expert_location_metadata(): return logger.info( "[EPLBManager] rebalance layout after:\n%s", format_expert_location_layout( get_global_expert_location_metadata(), layer_ids=update_layer_ids, ), ) def _chunk_list(items: List, chunk_size): for start_index in range(0, len(items), chunk_size): yield items[start_index : start_index + chunk_size]