Add explicit EPLB balancedness reporting modes (#34998)

This commit is contained in:
Lianmin Zheng
2026-08-16 15:31:11 -07:00
committed by GitHub
parent 77cadf6b98
commit f61f584347
10 changed files with 133 additions and 50 deletions
@@ -1885,10 +1885,10 @@ Please consult the documentation below and [server_args.py](https://github.com/s
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Type: int</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Type: int</td>
</tr> </tr>
<tr> <tr>
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}>`--enable-expert-distribution-metrics`</td> <td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}>`--expert-balancedness-report-mode`</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Enable logging metrics for expert balancedness</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Where to report expert balancedness.</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}>`False`</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}><code>off</code></td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>bool flag (set to enable)</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}><code>off</code>, <code>server_log</code>, <code>prometheus</code>, <code>both</code></td>
</tr> </tr>
<tr> <tr>
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}>`--deepep-config`</td> <td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}>`--deepep-config`</td>
@@ -1709,9 +1709,9 @@ If the value is int8, you must also set the environment variable:DEEP_NORMAL_MOD
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>A2, A3</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>A2, A3</td>
</tr> </tr>
<tr> <tr>
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}>`--enable-expert-distribution-metrics`</td> <td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}>`--expert-balancedness-report-mode`</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>`False`</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}><code>off</code></td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}>bool flag (set to enable)</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}><code>off</code>, <code>server_log</code>, <code>prometheus</code>, <code>both</code></td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>A2, A3</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>A2, A3</td>
</tr> </tr>
<tr> <tr>
@@ -1745,7 +1745,7 @@ SGLang supports various environment variables that can be used to configure its
</tr> </tr>
<tr> <tr>
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}><code>SGLANG_ENABLE_EPLB_BALANCEDNESS_METRIC</code></td> <td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}><code>SGLANG_ENABLE_EPLB_BALANCEDNESS_METRIC</code></td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Emit an EPLB balancedness metric.</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Removed. Use <code>--expert-balancedness-report-mode=prometheus</code> to emit the EPLB balancedness metric.</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}><code>false</code></td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}><code>false</code></td>
</tr> </tr>
<tr> <tr>
@@ -30,12 +30,15 @@ def print_deprecated_warning(message: str):
class DeprecatedAction(argparse.Action): 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__( super(DeprecatedAction, self).__init__(
option_strings, dest, nargs=nargs, **kwargs option_strings, dest, nargs=nargs, **kwargs
) )
def __call__(self, parser, namespace, values, option_string=None): def __call__(self, parser, namespace, values, option_string=None):
if self.error_message is not None:
parser.error(self.error_message)
print_deprecated_warning( print_deprecated_warning(
f"The command line argument '{option_string}' is deprecated and will be removed in future versions." f"The command line argument '{option_string}' is deprecated and will be removed in future versions."
) )
-1
View File
@@ -939,7 +939,6 @@ class Envs:
SGLANG_LOG_EXPERT_LOCATION_METADATA = EnvBool(False) SGLANG_LOG_EXPERT_LOCATION_METADATA = EnvBool(False)
SGLANG_EXPERT_DISTRIBUTION_RECORDER_DIR = EnvStr("/tmp") SGLANG_EXPERT_DISTRIBUTION_RECORDER_DIR = EnvStr("/tmp")
SGLANG_EPLB_HEATMAP_COLLECTION_INTERVAL = EnvInt(0) SGLANG_EPLB_HEATMAP_COLLECTION_INTERVAL = EnvInt(0)
SGLANG_ENABLE_EPLB_BALANCEDNESS_METRIC = EnvBool(False)
# Chunk size for the rebalance expert-weight P2P exchange; set # Chunk size for the rebalance expert-weight P2P exchange; set
# >= num_physical_experts to submit a single batch_isend_irecv. # >= num_physical_experts to submit a single batch_isend_irecv.
SGLANG_EPLB_P2P_BATCH_CHUNK_SIZE = EnvIntWithAlias( SGLANG_EPLB_P2P_BATCH_CHUNK_SIZE = EnvIntWithAlias(
+39 -25
View File
@@ -22,7 +22,17 @@ from collections import deque
from contextlib import contextmanager from contextlib import contextmanager
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path 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 einops
import torch import torch
@@ -47,16 +57,22 @@ logger = logging.getLogger(__name__)
# --------------------------------------- Entrypoint ----------------------------------------- # --------------------------------------- Entrypoint -----------------------------------------
_OutputMode = Literal["file", "object"] _OutputMode = Literal["file", "object"]
EPLB_BALANCEDNESS_WINDOW_SIZES = (10, 100, 1000)
@dataclass @dataclass
class ExpertDistributionMetrics: class ExpertDistributionMetrics:
forward_pass_id: int
eplb_balancedness: torch.Tensor eplb_balancedness: torch.Tensor
gpu_physical_count_sum: Optional[torch.Tensor]
reset_server_log_history: bool
def map_device_tensors(self, fn): def map_device_tensors(self, fn):
# Device-tensor fields only; caller injects the copy+safety primitive # Device-tensor fields only; caller injects the copy+safety primitive
# (see GenerationBatchResult.copy_to_cpu). # (see GenerationBatchResult.copy_to_cpu).
self.eplb_balancedness = fn(self.eplb_balancedness) 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): class ExpertDistributionRecorder(ABC):
@@ -159,9 +175,10 @@ class _ExpertDistributionRecorderReal(ExpertDistributionRecorder):
for k in self._accumulator.get_single_pass_gatherer_keys() 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( 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() self.start_record()
@@ -699,11 +716,12 @@ class _UtilizationRateAccumulatorMixin(_Accumulator):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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: if self._enable:
self.window_sizes = [10, 100, 1000] self.window_sizes = EPLB_BALANCEDNESS_WINDOW_SIZES
self._history = _DequeCollection(maxlens=self.window_sizes) self._history = _DequeCollection(maxlens=self.window_sizes)
self._reset_server_log_history = True
self._rank = torch.distributed.get_rank() self._rank = torch.distributed.get_rank()
expert_dispatch_cls = resolve_collector_class( expert_dispatch_cls = resolve_collector_class(
self._server_args, self._server_args,
@@ -732,6 +750,7 @@ class _UtilizationRateAccumulatorMixin(_Accumulator):
super().reset() super().reset()
if self._enable: if self._enable:
self._history.clear() self._history.clear()
self._reset_server_log_history = True
def _append_utilization_rate( def _append_utilization_rate(
self, self,
@@ -757,27 +776,22 @@ class _UtilizationRateAccumulatorMixin(_Accumulator):
should_track_history = not math.isclose( should_track_history = not math.isclose(
self._server_args.eplb_min_rebalancing_utilization_threshold, 1.0 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( if should_track_history:
f"[Expert Balancedness] " self._history.append(utilization_rate_gpu.item())
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()]}"
)
# TODO refactor # TODO refactor
def _handle_metric_eplb_heatmap(self, gpu_physical_count: torch.Tensor): def _handle_metric_eplb_heatmap(self, gpu_physical_count: torch.Tensor):
@@ -804,7 +818,7 @@ class _UtilizationRateAccumulatorMixin(_Accumulator):
class _DequeCollection: class _DequeCollection:
def __init__(self, maxlens: List[int]): def __init__(self, maxlens: Sequence[int]):
self._dequeues = [deque(maxlen=maxlen) for maxlen in maxlens] self._dequeues = [deque(maxlen=maxlen) for maxlen in maxlens]
def append(self, value): def append(self, value):
@@ -5,12 +5,13 @@ import logging
import math import math
import tempfile import tempfile
import time import time
from collections import defaultdict from collections import defaultdict, deque
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Optional, Tuple, Union from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from sglang.srt.disaggregation.utils import DisaggregationMode from sglang.srt.disaggregation.utils import DisaggregationMode
from sglang.srt.environ import envs 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.schedule_batch import ScheduleBatch
from sglang.srt.managers.utils import GenerationBatchResult from sglang.srt.managers.utils import GenerationBatchResult
from sglang.srt.observability.metrics_collector import ( 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._init_metrics(self.tp_rank, self.pp_rank, self.dp_rank)
self._install_device_timer_on_runners() 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( def _init_metrics(
self, self,
@@ -951,16 +957,45 @@ class SchedulerMetricsReporter:
batch: ScheduleBatch, batch: ScheduleBatch,
result: Union[GenerationBatchResult, EmbeddingBatchResult], result: Union[GenerationBatchResult, EmbeddingBatchResult],
): ):
if not self.enable_metrics:
return
if not isinstance(result, GenerationBatchResult): if not isinstance(result, GenerationBatchResult):
return return
if (m := result.expert_distribution_metrics) is not None: if (m := result.expert_distribution_metrics) is not None:
self.metrics_collector.increment_eplb_balancedness( balancedness = m.eplb_balancedness.item()
forward_mode=batch.forward_mode.name.lower(),
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( def _emit_forward_pass_metrics(
self, self,
@@ -24,7 +24,6 @@ from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Set, Union from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Set, Union
from sglang.srt.disaggregation.utils import DisaggregationMode 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.model_executor.forward_batch_info import ForwardMode
from sglang.srt.observability.utils import exponential_buckets, generate_buckets from sglang.srt.observability.utils import exponential_buckets, generate_buckets
from sglang.srt.server_args import ServerArgs from sglang.srt.server_args import ServerArgs
@@ -240,10 +239,10 @@ class SchedulerMetricsCollector(_StatLoggerDIMixin):
def __init__( def __init__(
self, self,
labels: Dict[str, str], labels: Dict[str, str],
server_args: ServerArgs,
enable_lora: bool = False, enable_lora: bool = False,
enable_hierarchical_cache: bool = False, enable_hierarchical_cache: bool = False,
enable_streaming_session: bool = False, enable_streaming_session: bool = False,
server_args: Optional[ServerArgs] = None,
) -> None: ) -> None:
# We need to import prometheus_client after setting the env variable `PROMETHEUS_MULTIPROC_DIR` # We need to import prometheus_client after setting the env variable `PROMETHEUS_MULTIPROC_DIR`
from prometheus_client import Counter as _PromCounter from prometheus_client import Counter as _PromCounter
@@ -874,7 +873,8 @@ class SchedulerMetricsCollector(_StatLoggerDIMixin):
# ================================================================= # =================================================================
if ( if (
labels["moe_ep_rank"] == 0 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( self.eplb_balancedness = Summary(
name="sglang:eplb_balancedness", name="sglang:eplb_balancedness",
documentation="Balancedness of MoE in expert parallelism.", documentation="Balancedness of MoE in expert parallelism.",
+35 -4
View File
@@ -2409,9 +2409,11 @@ class ServerArgs:
"Circular buffer size of expert distribution recorder. Set to -1 to denote infinite buffer.", "Circular buffer size of expert distribution recorder. Set to -1 to denote infinite buffer.",
NS("exec.moe"), NS("exec.moe"),
] = None ] = None
enable_expert_distribution_metrics: A[ expert_balancedness_report_mode: A[
bool, "Enable logging metrics for expert balancedness", NS("exec.moe") Literal["off", "server_log", "prometheus", "both"],
] = False "Where to report expert balancedness. Options: off, server_log, prometheus, both.",
NS("exec.moe"),
] = "off"
deepep_config: A[ deepep_config: A[
Optional[str], Optional[str],
"Tuned DeepEP config suitable for your own cluster. It can be either a string with JSON content or a file path.", "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 ==== # ===== END TO BE REFACTORED ====
def _handle_expert_distribution_metrics(self): 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 is None
): ):
self.expert_distribution_recorder_mode = "stat" self.expert_distribution_recorder_mode = "stat"
@@ -8620,6 +8629,19 @@ class ServerArgs:
) )
# --- Deprecated argument registrations --- # --- 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( parser.add_argument(
"--stream-output", "--stream-output",
action=DeprecatedStoreTrueAction, action=DeprecatedStoreTrueAction,
@@ -9593,6 +9615,15 @@ class ServerArgs:
"dp_size": self.dp_size, "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: def m3_fp8_attn_gemm_enabled(args) -> bool:
"""Whether MiniMax-M3 attention GEMMs run in fp8 (no opt-in flag; active """Whether MiniMax-M3 attention GEMMs run in fp8 (no opt-in flag; active
@@ -51,7 +51,8 @@ class TestEplbMinRebalancingUtilizationThresholdBase(CustomTestCase):
50, 50,
"--expert-distribution-recorder-buffer-size", "--expert-distribution-recorder-buffer-size",
50, 50,
"--enable-expert-distribution-metrics", "--expert-balancedness-report-mode",
"server_log",
"--eplb-rebalance-layers-per-chunk", "--eplb-rebalance-layers-per-chunk",
"1", "1",
] ]