From f61f584347fdee1c448d02080b5554b94a28c91e Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Sun, 16 Aug 2026 15:31:11 -0700 Subject: [PATCH] Add explicit EPLB balancedness reporting modes (#34998) --- .../advanced_features/server_arguments.mdx | 8 +-- .../reference/support_features.mdx | 6 +- .../docs/references/environment_variables.mdx | 2 +- .../sglang/srt/arg_groups/argparse_actions.py | 5 +- python/sglang/srt/environ.py | 1 - python/sglang/srt/eplb/expert_distribution.py | 64 +++++++++++-------- .../scheduler_components/metrics_reporter.py | 49 ++++++++++++-- .../srt/observability/metrics_collector.py | 6 +- python/sglang/srt/server_args.py | 39 +++++++++-- ...b_min_rebalancing_utilization_threshold.py | 3 +- 10 files changed, 133 insertions(+), 50 deletions(-) diff --git a/docs/docs/advanced_features/server_arguments.mdx b/docs/docs/advanced_features/server_arguments.mdx index 5ee20bead..ac4291753 100644 --- a/docs/docs/advanced_features/server_arguments.mdx +++ b/docs/docs/advanced_features/server_arguments.mdx @@ -1885,10 +1885,10 @@ Please consult the documentation below and [server_args.py](https://github.com/s Type: int - `--enable-expert-distribution-metrics` - Enable logging metrics for expert balancedness - `False` - bool flag (set to enable) + `--expert-balancedness-report-mode` + Where to report expert balancedness. + off + off, server_log, prometheus, both `--deepep-config` diff --git a/docs/docs/hardware-platforms/ascend-npus/reference/support_features.mdx b/docs/docs/hardware-platforms/ascend-npus/reference/support_features.mdx index 734f0d3ff..4c0fe9429 100644 --- a/docs/docs/hardware-platforms/ascend-npus/reference/support_features.mdx +++ b/docs/docs/hardware-platforms/ascend-npus/reference/support_features.mdx @@ -1709,9 +1709,9 @@ If the value is int8, you must also set the environment variable:DEEP_NORMAL_MOD A2, A3 - `--enable-expert-distribution-metrics` - `False` - bool flag (set to enable) + `--expert-balancedness-report-mode` + off + off, server_log, prometheus, both A2, A3 diff --git a/docs/docs/references/environment_variables.mdx b/docs/docs/references/environment_variables.mdx index 7bca0227e..a0248b71e 100644 --- a/docs/docs/references/environment_variables.mdx +++ b/docs/docs/references/environment_variables.mdx @@ -1745,7 +1745,7 @@ SGLang supports various environment variables that can be used to configure its SGLANG_ENABLE_EPLB_BALANCEDNESS_METRIC - Emit an EPLB balancedness metric. + Removed. Use --expert-balancedness-report-mode=prometheus to emit the EPLB balancedness metric. false diff --git a/python/sglang/srt/arg_groups/argparse_actions.py b/python/sglang/srt/arg_groups/argparse_actions.py index 3fdd39e97..06b98ceb4 100644 --- a/python/sglang/srt/arg_groups/argparse_actions.py +++ b/python/sglang/srt/arg_groups/argparse_actions.py @@ -30,12 +30,15 @@ def print_deprecated_warning(message: str): class DeprecatedAction(argparse.Action): - def __init__(self, option_strings, dest, nargs=0, **kwargs): + def __init__(self, option_strings, dest, error_message=None, nargs=0, **kwargs): + self.error_message = error_message super(DeprecatedAction, self).__init__( option_strings, dest, nargs=nargs, **kwargs ) def __call__(self, parser, namespace, values, option_string=None): + if self.error_message is not None: + parser.error(self.error_message) print_deprecated_warning( f"The command line argument '{option_string}' is deprecated and will be removed in future versions." ) diff --git a/python/sglang/srt/environ.py b/python/sglang/srt/environ.py index 5e8799ce4..6e0a8a62b 100644 --- a/python/sglang/srt/environ.py +++ b/python/sglang/srt/environ.py @@ -939,7 +939,6 @@ class Envs: SGLANG_LOG_EXPERT_LOCATION_METADATA = EnvBool(False) SGLANG_EXPERT_DISTRIBUTION_RECORDER_DIR = EnvStr("/tmp") SGLANG_EPLB_HEATMAP_COLLECTION_INTERVAL = EnvInt(0) - SGLANG_ENABLE_EPLB_BALANCEDNESS_METRIC = EnvBool(False) # Chunk size for the rebalance expert-weight P2P exchange; set # >= num_physical_experts to submit a single batch_isend_irecv. SGLANG_EPLB_P2P_BATCH_CHUNK_SIZE = EnvIntWithAlias( diff --git a/python/sglang/srt/eplb/expert_distribution.py b/python/sglang/srt/eplb/expert_distribution.py index e28f583ee..2ea7cb7d8 100644 --- a/python/sglang/srt/eplb/expert_distribution.py +++ b/python/sglang/srt/eplb/expert_distribution.py @@ -22,7 +22,17 @@ from collections import deque from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Type +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Literal, + Optional, + Sequence, + Tuple, + Type, +) import einops import torch @@ -47,16 +57,22 @@ logger = logging.getLogger(__name__) # --------------------------------------- Entrypoint ----------------------------------------- _OutputMode = Literal["file", "object"] +EPLB_BALANCEDNESS_WINDOW_SIZES = (10, 100, 1000) @dataclass class ExpertDistributionMetrics: + forward_pass_id: int eplb_balancedness: torch.Tensor + gpu_physical_count_sum: Optional[torch.Tensor] + reset_server_log_history: bool def map_device_tensors(self, fn): # Device-tensor fields only; caller injects the copy+safety primitive # (see GenerationBatchResult.copy_to_cpu). self.eplb_balancedness = fn(self.eplb_balancedness) + if self.gpu_physical_count_sum is not None: + self.gpu_physical_count_sum = fn(self.gpu_physical_count_sum) class ExpertDistributionRecorder(ABC): @@ -159,9 +175,10 @@ class _ExpertDistributionRecorderReal(ExpertDistributionRecorder): for k in self._accumulator.get_single_pass_gatherer_keys() } - if server_args.enable_expert_distribution_metrics: + if server_args.should_report_expert_balancedness(): logger.info( - "ExpertDistributionRecorder auto start record since enable_expert_distribution_metrics" + "ExpertDistributionRecorder auto start record since " + f"expert_balancedness_report_mode={server_args.expert_balancedness_report_mode}" ) self.start_record() @@ -699,11 +716,12 @@ class _UtilizationRateAccumulatorMixin(_Accumulator): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._enable = self._server_args.enable_expert_distribution_metrics + self._enable = self._server_args.should_report_expert_balancedness() if self._enable: - self.window_sizes = [10, 100, 1000] + self.window_sizes = EPLB_BALANCEDNESS_WINDOW_SIZES self._history = _DequeCollection(maxlens=self.window_sizes) + self._reset_server_log_history = True self._rank = torch.distributed.get_rank() expert_dispatch_cls = resolve_collector_class( self._server_args, @@ -732,6 +750,7 @@ class _UtilizationRateAccumulatorMixin(_Accumulator): super().reset() if self._enable: self._history.clear() + self._reset_server_log_history = True def _append_utilization_rate( self, @@ -757,27 +776,22 @@ class _UtilizationRateAccumulatorMixin(_Accumulator): should_track_history = not math.isclose( self._server_args.eplb_min_rebalancing_utilization_threshold, 1.0 ) - if envs.SGLANG_ENABLE_EPLB_BALANCEDNESS_METRIC.get(): - outputs["metrics"] = ExpertDistributionMetrics( - eplb_balancedness=utilization_rate_gpu, - ) - if should_track_history: - self._history.append(utilization_rate_gpu.item()) - else: - # TODO maybe refactor this part to also avoid a `.item()` gpu->cpu sync - utilization_rate_cpu = utilization_rate_gpu.item() - self._history.append(utilization_rate_cpu) - gpu_physical_count_sum = gpu_physical_count.sum().item() + should_log = ( + self._server_args.should_log_expert_balancedness_to_server_log() + ) + outputs["metrics"] = ExpertDistributionMetrics( + forward_pass_id=forward_pass_id, + eplb_balancedness=utilization_rate_gpu, + gpu_physical_count_sum=( + gpu_physical_count.sum() if should_log else None + ), + reset_server_log_history=self._reset_server_log_history, + ) + self._reset_server_log_history = False - logger.info( - f"[Expert Balancedness] " - f"forward_pass_id={forward_pass_id} " - f"current_pass_balancedness={utilization_rate_cpu:.03f} " - f"{''.join(f'last_{size}_average_balancedness={value:.03f} ' for size, value in self._history.mean().items())} " - f"gpu_physical_count_sum={gpu_physical_count_sum}" - # f"current_pass_per_layer={[round(x, 2) for x in utilization_rate_tensor.cpu().tolist()]}" - ) + if should_track_history: + self._history.append(utilization_rate_gpu.item()) # TODO refactor def _handle_metric_eplb_heatmap(self, gpu_physical_count: torch.Tensor): @@ -804,7 +818,7 @@ class _UtilizationRateAccumulatorMixin(_Accumulator): class _DequeCollection: - def __init__(self, maxlens: List[int]): + def __init__(self, maxlens: Sequence[int]): self._dequeues = [deque(maxlen=maxlen) for maxlen in maxlens] def append(self, value): diff --git a/python/sglang/srt/managers/scheduler_components/metrics_reporter.py b/python/sglang/srt/managers/scheduler_components/metrics_reporter.py index 73a49a66d..60bb04953 100644 --- a/python/sglang/srt/managers/scheduler_components/metrics_reporter.py +++ b/python/sglang/srt/managers/scheduler_components/metrics_reporter.py @@ -5,12 +5,13 @@ import logging import math import tempfile import time -from collections import defaultdict +from collections import defaultdict, deque from dataclasses import dataclass from typing import TYPE_CHECKING, List, Optional, Tuple, Union from sglang.srt.disaggregation.utils import DisaggregationMode from sglang.srt.environ import envs +from sglang.srt.eplb.expert_distribution import EPLB_BALANCEDNESS_WINDOW_SIZES from sglang.srt.managers.schedule_batch import ScheduleBatch from sglang.srt.managers.utils import GenerationBatchResult from sglang.srt.observability.metrics_collector import ( @@ -112,6 +113,11 @@ class SchedulerMetricsReporter: ) self._init_metrics(self.tp_rank, self.pp_rank, self.dp_rank) self._install_device_timer_on_runners() + # Keep log history after the existing async result copy so reporting does + # not synchronize the model stream once per generated token. + self._eplb_balancedness_history = [ + deque(maxlen=window_size) for window_size in EPLB_BALANCEDNESS_WINDOW_SIZES + ] def _init_metrics( self, @@ -951,16 +957,45 @@ class SchedulerMetricsReporter: batch: ScheduleBatch, result: Union[GenerationBatchResult, EmbeddingBatchResult], ): - if not self.enable_metrics: - return if not isinstance(result, GenerationBatchResult): return if (m := result.expert_distribution_metrics) is not None: - self.metrics_collector.increment_eplb_balancedness( - forward_mode=batch.forward_mode.name.lower(), - balancedness=m.eplb_balancedness.item(), - ) + balancedness = m.eplb_balancedness.item() + + if ( + self.scheduler.server_args.should_log_expert_balancedness_to_server_log() + ): + if m.reset_server_log_history: + for history in self._eplb_balancedness_history: + history.clear() + for history in self._eplb_balancedness_history: + history.append(balancedness) + balancedness_history_means = { + history.maxlen: sum(history) / len(history) + for history in self._eplb_balancedness_history + if len(history) > 0 + } + assert m.gpu_physical_count_sum is not None + gpu_physical_count_sum = m.gpu_physical_count_sum.item() + + logger.info( + f"[Expert Balancedness] " + f"forward_pass_id={m.forward_pass_id} " + f"current_pass_balancedness={balancedness:.03f} " + f"{''.join(f'last_{size}_average_balancedness={value:.03f} ' for size, value in balancedness_history_means.items())} " + f"gpu_physical_count_sum={gpu_physical_count_sum}" + ) + + if ( + self.enable_metrics + and self.scheduler.server_args.should_export_expert_balancedness_to_prometheus() + ): + assert self.metrics_collector is not None + self.metrics_collector.increment_eplb_balancedness( + forward_mode=batch.forward_mode.name.lower(), + balancedness=balancedness, + ) def _emit_forward_pass_metrics( self, diff --git a/python/sglang/srt/observability/metrics_collector.py b/python/sglang/srt/observability/metrics_collector.py index a5976c120..adc15d97a 100644 --- a/python/sglang/srt/observability/metrics_collector.py +++ b/python/sglang/srt/observability/metrics_collector.py @@ -24,7 +24,6 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Set, Union from sglang.srt.disaggregation.utils import DisaggregationMode -from sglang.srt.environ import envs from sglang.srt.model_executor.forward_batch_info import ForwardMode from sglang.srt.observability.utils import exponential_buckets, generate_buckets from sglang.srt.server_args import ServerArgs @@ -240,10 +239,10 @@ class SchedulerMetricsCollector(_StatLoggerDIMixin): def __init__( self, labels: Dict[str, str], + server_args: ServerArgs, enable_lora: bool = False, enable_hierarchical_cache: bool = False, enable_streaming_session: bool = False, - server_args: Optional[ServerArgs] = None, ) -> None: # We need to import prometheus_client after setting the env variable `PROMETHEUS_MULTIPROC_DIR` from prometheus_client import Counter as _PromCounter @@ -874,7 +873,8 @@ class SchedulerMetricsCollector(_StatLoggerDIMixin): # ================================================================= if ( labels["moe_ep_rank"] == 0 - ) and envs.SGLANG_ENABLE_EPLB_BALANCEDNESS_METRIC.get(): + and server_args.should_export_expert_balancedness_to_prometheus() + ): self.eplb_balancedness = Summary( name="sglang:eplb_balancedness", documentation="Balancedness of MoE in expert parallelism.", diff --git a/python/sglang/srt/server_args.py b/python/sglang/srt/server_args.py index 4f23cc0d8..bebbf3194 100644 --- a/python/sglang/srt/server_args.py +++ b/python/sglang/srt/server_args.py @@ -2409,9 +2409,11 @@ class ServerArgs: "Circular buffer size of expert distribution recorder. Set to -1 to denote infinite buffer.", NS("exec.moe"), ] = None - enable_expert_distribution_metrics: A[ - bool, "Enable logging metrics for expert balancedness", NS("exec.moe") - ] = False + expert_balancedness_report_mode: A[ + Literal["off", "server_log", "prometheus", "both"], + "Where to report expert balancedness. Options: off, server_log, prometheus, both.", + NS("exec.moe"), + ] = "off" deepep_config: A[ Optional[str], "Tuned DeepEP config suitable for your own cluster. It can be either a string with JSON content or a file path.", @@ -7198,7 +7200,14 @@ class ServerArgs: # ===== END TO BE REFACTORED ==== def _handle_expert_distribution_metrics(self): - if self.enable_expert_distribution_metrics and ( + if "SGLANG_ENABLE_EPLB_BALANCEDNESS_METRIC" in os.environ: + raise ValueError( + "SGLANG_ENABLE_EPLB_BALANCEDNESS_METRIC is no longer supported. Use " + "--expert-balancedness-report-mode with one of: off, server_log, " + "prometheus, both." + ) + + if self.should_report_expert_balancedness() and ( self.expert_distribution_recorder_mode is None ): self.expert_distribution_recorder_mode = "stat" @@ -8620,6 +8629,19 @@ class ServerArgs: ) # --- Deprecated argument registrations --- + parser.add_argument( + "--enable-expert-distribution-metrics", + action=DeprecatedAction, + error_message=( + "--enable-expert-distribution-metrics is no longer supported. Use " + "--expert-balancedness-report-mode with one of: off, server_log, " + "prometheus, both." + ), + help=( + "Removed. Use --expert-balancedness-report-mode with one of: " + "off, server_log, prometheus, both." + ), + ) parser.add_argument( "--stream-output", action=DeprecatedStoreTrueAction, @@ -9593,6 +9615,15 @@ class ServerArgs: "dp_size": self.dp_size, } + def should_report_expert_balancedness(self) -> bool: + return self.expert_balancedness_report_mode != "off" + + def should_log_expert_balancedness_to_server_log(self) -> bool: + return self.expert_balancedness_report_mode in ("server_log", "both") + + def should_export_expert_balancedness_to_prometheus(self) -> bool: + return self.expert_balancedness_report_mode in ("prometheus", "both") + def m3_fp8_attn_gemm_enabled(args) -> bool: """Whether MiniMax-M3 attention GEMMs run in fp8 (no opt-in flag; active diff --git a/test/registered/npu/basic_function/parallel_strategy/expert_parallelism/test_npu_eplb_min_rebalancing_utilization_threshold.py b/test/registered/npu/basic_function/parallel_strategy/expert_parallelism/test_npu_eplb_min_rebalancing_utilization_threshold.py index 1c23e7df1..dcba511fe 100644 --- a/test/registered/npu/basic_function/parallel_strategy/expert_parallelism/test_npu_eplb_min_rebalancing_utilization_threshold.py +++ b/test/registered/npu/basic_function/parallel_strategy/expert_parallelism/test_npu_eplb_min_rebalancing_utilization_threshold.py @@ -51,7 +51,8 @@ class TestEplbMinRebalancingUtilizationThresholdBase(CustomTestCase): 50, "--expert-distribution-recorder-buffer-size", 50, - "--enable-expert-distribution-metrics", + "--expert-balancedness-report-mode", + "server_log", "--eplb-rebalance-layers-per-chunk", "1", ]