Record a process's placement at publish, not at group build (#40071)

This commit is contained in:
Cheng Wan
2026-09-18 23:52:37 -07:00
committed by GitHub
parent 36aa8479ef
commit 3a5f52e144
47 changed files with 917 additions and 231 deletions
+18 -2
View File
@@ -93,10 +93,12 @@ from sglang.srt.model_executor.cuda_graph_config import (
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.model_executor.model_runner import ModelRunner
from sglang.srt.runtime_context import (
SpawnRanks,
get_model,
get_parallel,
get_schedule,
publish,
spawn_world_rank,
)
from sglang.srt.sampling.sampling_params import SamplingParams
from sglang.srt.server_args import PortArgs, ServerArgs
@@ -707,7 +709,15 @@ def correctness_test(
gpu_id,
tp_rank,
):
publish(server_args, role="scheduler")
# With the placement this process was spawned with, so a rank read here
# does not need a process group -- the same bundle the runner is handed.
publish(
server_args,
role="scheduler",
ranks=SpawnRanks(
world_rank=spawn_world_rank(server_args, tp_rank=tp_rank, pp_rank=0)
),
)
# Configure the logger
configure_logger(server_args, prefix=f" TP{tp_rank}")
@@ -912,7 +922,13 @@ def latency_test(
cfg = resolving_view(server_args)
# `main` runs this inline for tp_size == 1 and spawns it per rank otherwise;
# a spawned child arrives with nothing published.
publish(server_args, role="scheduler")
publish(
server_args,
role="scheduler",
ranks=SpawnRanks(
world_rank=spawn_world_rank(server_args, tp_rank=tp_rank, pp_rank=0)
),
)
initialize_moe_config()
initialize_fp8_gemm_config()
initialize_fp4_gemm_config()
@@ -14,7 +14,7 @@ from sglang.srt.constrained.base_grammar_backend import (
from sglang.srt.constrained.reasoner_grammar_backend import ReasonerGrammarObject
from sglang.srt.distributed.communication_tags import P2PTag
from sglang.srt.environ import envs
from sglang.srt.runtime_context import get_serving
from sglang.srt.runtime_context import get_parallel, get_serving
from sglang.srt.sampling.sampling_params import (
get_request_reasoning_end_token_ids,
)
@@ -53,8 +53,8 @@ class GrammarManager:
self.grammar_sync_size = scheduler.dp_tp_group.world_size
self.grammar_sync_entry = scheduler.dp_tp_group.first_rank
self.is_grammar_sync_entry = scheduler.dp_tp_group.is_first_rank
self.pp_rank = scheduler.ps.pp_rank
self.pp_size = scheduler.ps.pp_size
self.pp_rank = get_parallel().pp_rank
self.pp_size = get_parallel().pp_size
self.pp_group = scheduler.pp_group
self.grammar_pp_sync_work_list = []
+1 -1
View File
@@ -393,7 +393,7 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
self.bootstrap_port = bootstrap_port
self.max_total_num_tokens = max_total_num_tokens
self.pp_rank = pp_rank
self.pp_size = scheduler.ps.pp_size
self.pp_size = get_parallel().pp_size
self.num_reserved_decode_tokens = num_reserved_decode_tokens
self.transfer_backend = transfer_backend
# Queue for requests pending pre-allocation
+3 -3
View File
@@ -997,7 +997,7 @@ class SchedulerDisaggregationPrefillMixin:
KVPoll.Failed,
):
logger.warning_once(
f"PP rank {self.ps.pp_rank}: unexpected poll state {poll} for rid {req.rid} "
f"PP rank {get_parallel().pp_rank}: unexpected poll state {poll} for rid {req.rid} "
f"from consensus; treating as undone",
)
undone_reqs.append(req)
@@ -1077,7 +1077,7 @@ class SchedulerDisaggregationPrefillMixin:
) -> Optional[Exception]:
"""Conclude an inflight request whose KV transfer failed."""
error_message = (
f"Prefill transfer failed for request rank={self.ps.tp_rank} "
f"Prefill transfer failed for request rank={get_parallel().tp_rank} "
f"{req.rid=} {req.bootstrap_room=}"
)
exc: Optional[Exception] = None
@@ -1142,7 +1142,7 @@ class SchedulerDisaggregationPrefillMixin:
def handle_bootstrap_failure(self: Scheduler, req: Req) -> None:
self.clear_pending_chunk_send(req)
error_message = (
f"Prefill bootstrap failed for request rank={self.ps.tp_rank} "
f"Prefill bootstrap failed for request rank={get_parallel().tp_rank} "
f"{req.rid=} {req.bootstrap_room=}"
)
is_propagated = False
@@ -99,6 +99,7 @@ def init_torch_distributed(
)
# Only initialize the distributed environment on the target model worker.
# This builds the groups behind the context's live group-handle reads.
_init_parallel_groups(
backend=backend,
dist_init_method=dist_init_method,
@@ -3029,7 +3029,14 @@ def patch_pipeline_parallel_group(pp_group: GroupCoordinator):
global _PP
_PP = pp_group
try:
yield
# `pp_size` is a configured leaf: unlike the rank and the handle it
# does not follow the group being swapped, so the scope has to name it.
with get_parallel().override(
pp_size=pp_group.world_size,
pp_rank=pp_group.rank_in_group,
pp_group=pp_group,
):
yield
finally:
_PP_STATE_PATCHED = False
_PP = old_pp_group
@@ -509,7 +509,7 @@ def pp_parallel_deep_gemm_warmup(runner) -> None:
logger.info(
"PP-parallel DeepGEMM warmup start "
"(pp_rank=%d, tp_rank=%d, batch_sizes=%s, disagg=%s).",
model_runner.ps.pp_rank,
get_parallel().pp_rank,
model_runner.ps.tp_rank,
batch_sizes,
disagg_mode,
@@ -538,5 +538,5 @@ def pp_parallel_deep_gemm_warmup(runner) -> None:
logger.info(
"PP-parallel DeepGEMM warmup done in %.2fs (pp_rank=%d).",
time.perf_counter() - t0,
model_runner.ps.pp_rank,
get_parallel().pp_rank,
)
+7 -9
View File
@@ -27,6 +27,7 @@ from sglang.srt.distributed.device_communicators.pynccl_allocator import (
)
from sglang.srt.environ import envs
from sglang.srt.runtime_context import (
derive_attention_ranks,
derive_attention_widths,
get_device,
get_exec,
@@ -349,15 +350,12 @@ def compute_dp_attention_world_info(
dp_size=dp_size,
enable_dp_attention=enable_dp_attention,
)
attn_tp_rank = tp_rank % attn_tp_size
if not enable_dp_attention:
attn_dp_rank = 0
else:
# Rank layout is (dp, cp, tp) where tp is the fastest-changing dim:
# tp_rank = (attn_dp_rank * attn_cp_size + attn_cp_rank) * attn_tp_size + attn_tp_rank
attn_dp_rank = tp_rank // (attn_tp_size * attn_cp_size)
attn_tp_rank, attn_dp_rank = derive_attention_ranks(
tp_rank=tp_rank,
attn_tp_size=attn_tp_size,
attn_cp_size=attn_cp_size,
enable_dp_attention=enable_dp_attention,
)
return attn_tp_rank, attn_tp_size, attn_dp_rank, attn_dp_size
+98 -67
View File
@@ -29,6 +29,7 @@ from http import HTTPStatus
from typing import TYPE_CHECKING, Any, Deque, Dict, List, Optional, Set, Tuple, Union
from sglang.srt.runtime_context import (
SpawnRanks,
attention_backends,
get_context,
get_device,
@@ -44,6 +45,7 @@ from sglang.srt.runtime_context import (
get_serving,
get_spec,
publish,
spawn_world_rank,
)
from sglang.srt.utils.common import suppress_noisy_warnings # isort: skip
@@ -595,9 +597,9 @@ class Scheduler(
enable_metrics=get_observability().enable_metrics,
enable_kv_cache_events=bool(
get_observability().kv_events_config
and self.ps.pp_rank == 0
and self.ps.attn_tp_rank == 0
and self.ps.attn_cp_rank == 0
and get_parallel().pp_rank == 0
and get_parallel().attn_tp_rank == 0
and get_parallel().attn_cp_rank == 0
),
ps=self.ps,
tp_group=self.tp_group,
@@ -759,8 +761,8 @@ class Scheduler(
return
rank = (
self.ps.dp_rank
if self.ps.dp_rank is not None
get_parallel().dp_rank
if get_parallel().dp_rank is not None
else self.tp_group.rank_in_group
)
logger.info("HCCL DP prewarm start: rank=%s", rank)
@@ -775,10 +777,10 @@ class Scheduler(
if _is_npu:
from sglang.srt.hardware_backend.npu.utils import init_zbal
if self.ps.pp_size > 1:
if get_parallel().pp_size > 1:
logger.error("only zbal mix mode support pp_size > 1!")
init_zbal(
self.ps.tp_size, self.ps.gpu_id, self.ps.tp_rank
get_parallel().tp_size, self.ps.gpu_id, get_parallel().tp_rank
) # only switch allocator if is mix mode
def init_model_config(self):
@@ -811,9 +813,9 @@ class Scheduler(
def init_ipc_channels(self, port_args: PortArgs):
is_rank_zero = (
self.ps.pp_rank == 0
and self.ps.attn_tp_rank == 0
and self.ps.attn_cp_rank == 0
get_parallel().pp_rank == 0
and get_parallel().attn_tp_rank == 0
and get_parallel().attn_cp_rank == 0
)
self.ipc_channels = SchedulerIpcChannels.create(
port_args=port_args,
@@ -824,7 +826,7 @@ class Scheduler(
skip_tokenizer_init=self.skip_tokenizer_init,
metrics_enabled=get_observability().enable_metrics
and (
self.ps.attn_tp_rank == 0
get_parallel().attn_tp_rank == 0
or get_observability().enable_metrics_for_all_schedulers
),
enable_scripted_runtime=envs.SGLANG_TEST_SCRIPTED_RUNTIME.get(),
@@ -837,7 +839,7 @@ class Scheduler(
return
self.recv_from_tokenizer = self.ipc_channels.recv_from_tokenizer
dp_rank = self.ps.dp_rank if self.ps.dp_rank is not None else 0
dp_rank = get_parallel().dp_rank if get_parallel().dp_rank is not None else 0
try:
self.load_snapshot_writer = create_load_snapshot_writer(
port_args,
@@ -850,9 +852,9 @@ class Scheduler(
def init_idle_sleeper(self) -> None:
if (
self.ps.pp_rank == 0
and self.ps.attn_tp_rank == 0
and self.ps.attn_cp_rank == 0
get_parallel().pp_rank == 0
and get_parallel().attn_tp_rank == 0
and get_parallel().attn_cp_rank == 0
and get_device().sleep_on_idle
):
self.idle_sleeper = IdleSleeper(
@@ -1032,8 +1034,8 @@ class Scheduler(
if (
envs.SGLANG_ENABLE_PP_SPEC.get()
and self.ps.pp_size > 1
and self.ps.pp_rank != self.ps.pp_size - 1
and get_parallel().pp_size > 1
and get_parallel().pp_rank != get_parallel().pp_size - 1
):
# PP+spec: the draft model (MTP layer) needs final hidden states and
# the lm_head, both of which live on the last PP stage only.
@@ -1130,7 +1132,7 @@ class Scheduler(
# Match run_batch / _pp_launch_batch so warmup allocations stay reusable.
forward_stream = (
model_runner.forward_stream
if self.enable_overlap or self.ps.pp_size > 1 or use_mlx()
if self.enable_overlap or get_parallel().pp_size > 1 or use_mlx()
else self.schedule_stream
)
with device_module.stream(forward_stream):
@@ -1208,7 +1210,7 @@ class Scheduler(
get_context().override(
"scheduler.pp_max_micro_batch_size_default",
pp_max_micro_batch_size=max(
self.max_running_requests // self.ps.pp_size, 1
self.max_running_requests // get_parallel().pp_size, 1
),
)
@@ -1239,7 +1241,7 @@ class Scheduler(
self.startup_available_gpu_memory_gb = get_available_gpu_memory(
self.device, self.ps.gpu_id, empty_cache=False
)
if self.ps.tp_rank == 0:
if get_parallel().tp_rank == 0:
logger.info(
f"max_total_num_tokens={self.max_total_num_tokens}, "
f"chunked_prefill_size={get_schedule().chunked_prefill_size}, "
@@ -1344,7 +1346,7 @@ class Scheduler(
def maybe_init_dynamic_chunk_sizer(self) -> None:
"""Profile a PP prefill latency model that sizes chunks per stage."""
self.dynamic_chunk_sizer: Optional[DynamicChunkSizer] = None
if not (get_schedule().enable_dynamic_chunking and self.ps.pp_size > 1):
if not (get_schedule().enable_dynamic_chunking and get_parallel().pp_size > 1):
return
sizer = DynamicChunkSizer(
model_runner=self.tp_worker.model_runner,
@@ -1359,7 +1361,7 @@ class Scheduler(
device=self.device,
pp_group=self.pp_group,
world_group=self.world_group,
pp_rank=self.ps.pp_rank,
pp_rank=get_parallel().pp_rank,
)
if sizer.profile_and_fit():
self.dynamic_chunk_sizer = sizer
@@ -1422,7 +1424,7 @@ class Scheduler(
else:
self.prefill_delayer = PrefillDelayer(
dp_size=self.ps.dp_size,
attn_tp_size=self.ps.attn_tp_size,
attn_tp_size=get_parallel().attn_tp_size,
cpu_group=self.tp_cpu_group,
device_group=self.tp_group.device_group,
metrics_collector=(
@@ -1433,7 +1435,7 @@ class Scheduler(
max_delay_passes=get_schedule().prefill_delayer_max_delay_passes,
token_usage_low_watermark=get_schedule().prefill_delayer_token_usage_low_watermark,
device=self.tp_group.device,
debug_log_enabled=self.ps.attn_tp_rank == 0,
debug_log_enabled=get_parallel().attn_tp_rank == 0,
)
# NOTE: preemption is enabled by default for priority scheduling.
@@ -1464,7 +1466,7 @@ class Scheduler(
# Init recv skipper and input blocker
self.recv_skipper = SchedulerRecvSkipper.maybe_create()
self.input_blocker = (
SchedulerInputBlocker(noop=self.ps.attn_tp_rank != 0)
SchedulerInputBlocker(noop=get_parallel().attn_tp_rank != 0)
if get_bool_env_var("SGLANG_ENABLE_COLOCATED_BATCH_GEN")
else None
)
@@ -1553,7 +1555,7 @@ class Scheduler(
self.disagg_decode_transfer_queue = DecodeTransferQueue(
gloo_group=self.attn_tp_cpu_group,
req_to_metadata_buffer_idx_allocator=self.req_to_metadata_buffer_idx_allocator,
tp_rank=self.ps.tp_rank,
tp_rank=get_parallel().tp_rank,
metadata_buffers=self.disagg_metadata_buffers,
scheduler=self,
tree_cache=self.tree_cache,
@@ -1570,13 +1572,13 @@ class Scheduler(
transfer_queue=self.disagg_decode_transfer_queue,
tree_cache=self.tree_cache,
gloo_group=self.attn_tp_cpu_group,
tp_rank=self.ps.tp_rank,
tp_size=self.ps.tp_size,
tp_rank=get_parallel().tp_rank,
tp_size=get_parallel().tp_size,
dp_size=get_parallel().dp_size,
gpu_id=self.ps.gpu_id,
bootstrap_port=get_disagg().disaggregation_bootstrap_port,
max_total_num_tokens=self.max_total_num_tokens,
pp_rank=self.ps.pp_rank,
pp_rank=get_parallel().pp_rank,
num_reserved_decode_tokens=get_disagg().num_reserved_decode_tokens,
transfer_backend=self.transfer_backend,
)
@@ -1602,16 +1604,16 @@ class Scheduler(
draft_token_to_kv_pool=draft_token_to_kv_pool,
req_to_metadata_buffer_idx_allocator=self.req_to_metadata_buffer_idx_allocator,
metadata_buffers=self.disagg_metadata_buffers,
tp_rank=self.ps.tp_rank,
tp_size=self.ps.tp_size,
tp_rank=get_parallel().tp_rank,
tp_size=get_parallel().tp_size,
gpu_id=self.ps.gpu_id,
bootstrap_port=get_disagg().disaggregation_bootstrap_port,
gloo_group=self.attn_tp_cpu_group,
max_total_num_tokens=self.max_total_num_tokens,
scheduler=self,
scheduler_stage_metrics=self.scheduler_stage_metrics,
pp_rank=self.ps.pp_rank,
pp_size=self.ps.pp_size,
pp_rank=get_parallel().pp_rank,
pp_size=get_parallel().pp_size,
transfer_backend=self.transfer_backend,
)
# The prefill requests that are in the middle of kv sending
@@ -1638,8 +1640,8 @@ class Scheduler(
self.server_args,
dtype=self.model_config.dtype,
hf_config=self.model_config.hf_config,
pp_rank=self.ps.pp_rank,
tp_rank=self.ps.tp_rank,
pp_rank=get_parallel().pp_rank,
tp_rank=get_parallel().tp_rank,
tp_group=self.tp_group,
scheduler=self,
)
@@ -1895,7 +1897,9 @@ class Scheduler(
if self.device == "cpu":
self.schedule_stream.synchronize = lambda: None # No-op for CPU
elif (is_cuda() or _is_hip) and (self.enable_overlap or self.ps.pp_size > 1):
elif (is_cuda() or _is_hip) and (
self.enable_overlap or get_parallel().pp_size > 1
):
# CUDA/HIP streams come from a fixed round-robin pool. Redraw if this
# stream aliases forward_stream, which would eliminate scheduler
# overlap. Only CUDA/HIP streams expose a ``cuda_stream`` handle;
@@ -2090,9 +2094,9 @@ class Scheduler(
"""
local_reqs = []
if (
self.ps.pp_rank == 0
and self.ps.attn_tp_rank == 0
and self.ps.attn_cp_rank == 0
get_parallel().pp_rank == 0
and get_parallel().attn_tp_rank == 0
and get_parallel().attn_cp_rank == 0
):
local_reqs = self._poll_timeout_aborts()
recv_reqs = self.request_receiver.recv_requests(local_reqs=local_reqs)
@@ -2290,9 +2294,9 @@ class Scheduler(
and with it the server-process duties a Python ``TokenizerManager``
would otherwise own (e.g. serving the PD KV bootstrap registry)."""
return envs.SGLANG_RUST_SERVER.get() and (
self.ps.pp_rank == 0
and self.ps.attn_tp_rank == 0
and self.ps.attn_cp_rank == 0
get_parallel().pp_rank == 0
and get_parallel().attn_tp_rank == 0
and get_parallel().attn_cp_rank == 0
)
def maybe_init_rust_server(self) -> None:
@@ -2417,10 +2421,10 @@ class Scheduler(
self.kv_events_publisher = SchedulerKvEventsPublisher(
kv_events_config=get_observability().kv_events_config,
ps=self.ps,
attn_tp_rank=self.ps.attn_tp_rank,
attn_cp_rank=self.ps.attn_cp_rank,
attn_tp_rank=get_parallel().attn_tp_rank,
attn_cp_rank=get_parallel().attn_cp_rank,
attn_dp_rank=self.ps.attn_dp_rank,
dp_rank=self.ps.dp_rank,
dp_rank=get_parallel().dp_rank,
tree_cache=self.tree_cache,
send_metrics_from_scheduler=self.ipc_channels.send_metrics_from_scheduler,
max_running_requests=self.max_running_requests,
@@ -3398,7 +3402,7 @@ class Scheduler(
if (timeout_s := envs.SGLANG_REQ_RUNNING_TIMEOUT.get()) > 0:
deadline = time.perf_counter() - timeout_s
if self.ps.pp_size == 1:
if get_parallel().pp_size == 1:
inflight_batches = [self.running_batch, self.last_batch]
else:
inflight_batches = [*self.running_mbs, *self.mbs]
@@ -4460,7 +4464,7 @@ class Scheduler(
batch.input_ids = None
self._copy_auxiliary_output_to_cpu(batch, batch_result)
elif not batch.spec_algorithm.is_none():
is_verify_round = self.ps.pp_size > 1 and not (
is_verify_round = get_parallel().pp_size > 1 and not (
batch.forward_mode.is_extend() or batch.is_extend_in_batch
)
# The relayed tree is what the requests carry between rounds;
@@ -4548,7 +4552,10 @@ class Scheduler(
# Only the last PP rank owns real results requiring D2H; other ranks
# consume device tensors rebuilt from the output ring.
batch_result.copy_done = self.device_module.Event()
if batch_result.has_sampled_token_ids and self.ps.pp_size == 1:
if (
batch_result.has_sampled_token_ids
and get_parallel().pp_size == 1
):
batch_result.copy_to_cpu(
return_logprob=batch.return_logprob,
return_hidden_states=batch.return_hidden_states,
@@ -4685,7 +4692,7 @@ class Scheduler(
):
return
# PP transports the device output to the first rank before copying it.
if self.ps.pp_size > 1:
if get_parallel().pp_size > 1:
return
if result.copy_done is not None:
raise RuntimeError(
@@ -5024,7 +5031,7 @@ class Scheduler(
return idle
def _pp_microbatches_drained(self) -> bool:
if self.ps.pp_size == 1:
if get_parallel().pp_size == 1:
return True
return all(x.is_empty() for x in self.running_mbs) and all(
mb is None or mb.is_empty() for mb in self.mbs
@@ -5265,10 +5272,10 @@ class Scheduler(
if_success = False
break
elif k == "pp_max_micro_batch_size" and (
v > self.max_running_requests // self.ps.pp_size or v < 1
v > self.max_running_requests // get_parallel().pp_size or v < 1
):
logging.warning(
f"Updating {k} to {v} is rejected because it is out of the valid range [1, {self.max_running_requests // self.ps.pp_size}]."
f"Updating {k} to {v} is rejected because it is out of the valid range [1, {self.max_running_requests // get_parallel().pp_size}]."
)
if_success = False
break
@@ -5382,7 +5389,7 @@ class Scheduler(
)
def collect_inflight_reqs(self) -> Set[Req]:
if self.ps.pp_size == 1:
if get_parallel().pp_size == 1:
inflight_batches = [self.running_batch, self.last_batch]
else:
inflight_batches = [*self.running_mbs, *self.mbs]
@@ -5472,7 +5479,7 @@ class Scheduler(
if hasattr(req.disagg_kv_sender, "abort"):
req.disagg_kv_sender.abort()
if self.ps.pp_size > 1:
if get_parallel().pp_size > 1:
prepare_abort(req, "Aborted by AbortReq.")
# Abort in-flight requests
@@ -5488,7 +5495,7 @@ class Scheduler(
if recv_req.abort_all or decode_req.req.rid.startswith(recv_req.rid):
logger.debug(f"Abort prealloc queue request. {decode_req.req.rid=}")
decode_req.kv_receiver.abort()
if self.ps.pp_size > 1:
if get_parallel().pp_size > 1:
prepare_abort(decode_req.req, "Aborted by AbortReq.")
# Abort requests waiting for kvcache to release tree cache
@@ -5821,7 +5828,11 @@ class Scheduler(
output = self.session_controller.open(recv_req)
if output.success and self.enable_session_radix_cache:
self.tree_cache.open_radix_session(recv_req.session_id)
if self.ps.pp_rank == 0 and self.ps.tp_rank == 0 and self.ps.attn_cp_rank == 0:
if (
get_parallel().pp_rank == 0
and get_parallel().tp_rank == 0
and get_parallel().attn_cp_rank == 0
):
return output
return None
@@ -5922,6 +5933,20 @@ def _dispatch_event_loop_once(scheduler: Scheduler):
scheduler.event_loop_normal_disagg_decode()
def resolve_spawn_dp_rank(dp_rank: Optional[int]) -> Optional[int]:
"""The `dp_rank` this process was spawned with, in either of its two forms.
A router does not pass it as an argument, it sets `SGLANG_DP_RANK`. Both
forms are the launcher naming this process's place, so both have to be in
hand before `publish` records the placement -- resolving one of them after
would leave the context answering `None` for a process that has a rank.
"""
if dp_rank is None and "SGLANG_DP_RANK" in os.environ:
# [For Router] if env var "SGLANG_DP_RANK" exist, set dp_rank to the value of the env var
return int(os.environ["SGLANG_DP_RANK"])
return dp_rank
def configure_scheduler_process(
server_args: ServerArgs,
gpu_id: int,
@@ -5934,18 +5959,15 @@ def configure_scheduler_process(
display_tp_rank: Optional[int] = None,
display_dp_rank: Optional[int] = None,
display_moe_ep_rank: Optional[int] = None,
) -> Optional[int]:
) -> None:
"""Configure scheduler worker logging and process title.
display_* ranks are cosmetic; runtime ranks stay local.
display_* ranks are cosmetic; runtime ranks stay local. `dp_rank` arrives
already resolved -- see `resolve_spawn_dp_rank`.
"""
kill_itself_when_parent_died()
# Generate the logger prefix
if dp_rank is None and "SGLANG_DP_RANK" in os.environ:
# [For Router] if env var "SGLANG_DP_RANK" exist, set dp_rank to the value of the env var
dp_rank = int(os.environ["SGLANG_DP_RANK"])
shown_dp = display_dp_rank if display_dp_rank is not None else dp_rank
shown_tp = display_tp_rank if display_tp_rank is not None else tp_rank
shown_moe_ep = (
@@ -5987,8 +6009,6 @@ def configure_scheduler_process(
if numa_node is not None:
numa_bind_to_node(numa_node)
return dp_rank
def run_scheduler_process(
server_args: ServerArgs,
@@ -6007,9 +6027,20 @@ def run_scheduler_process(
):
# Load plugins so hooks can override Scheduler and its dependencies.
load_plugins()
# Publish before anything in this process reads configuration.
publish(server_args, role="scheduler")
dp_rank = configure_scheduler_process(
dp_rank = resolve_spawn_dp_rank(dp_rank)
# Publish before anything in this process reads configuration, with the
# placement the launcher decided: from here on a rank read is answered
# without a process group, which is what every reader needs before
# `init_torch_distributed` has run.
publish(
server_args,
role="scheduler",
ranks=SpawnRanks(
world_rank=spawn_world_rank(server_args, tp_rank=tp_rank, pp_rank=pp_rank),
dp_rank=dp_rank,
),
)
configure_scheduler_process(
server_args,
gpu_id,
tp_rank,
@@ -548,8 +548,8 @@ class SchedulerDPAttnAdapter:
local_batch,
model_runner=self.model_runner,
dp_size=get_parallel().dp_size,
attn_tp_size=self.ps.attn_tp_size,
attn_cp_size=self.ps.attn_cp_size,
attn_tp_size=get_parallel().attn_tp_size,
attn_cp_size=get_parallel().attn_cp_size,
tp_group=self.tp_group,
get_idle_batch=self.get_idle_batch,
disable_cuda_graph=cuda_graph_fully_disabled(),
@@ -19,6 +19,7 @@ from sglang.srt.disaggregation.kv_events import (
select_kv_publisher_dp_rank,
)
from sglang.srt.managers.io_struct import hook_custom_types, sock_send
from sglang.srt.runtime_context import get_parallel
if TYPE_CHECKING:
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
@@ -68,7 +69,7 @@ class SchedulerKvEventsPublisher:
self.kv_event_publisher = EventPublisherFactory.create(
kv_events_config,
select_kv_publisher_dp_rank(
self.ps.attn_dp_size, self.ps.attn_dp_rank, self.ps.dp_rank
self.ps.attn_dp_size, self.ps.attn_dp_rank, get_parallel().dp_rank
),
)
@@ -87,7 +88,7 @@ class SchedulerKvEventsPublisher:
kv_metrics.gpu_cache_usage_perc = self.get_stats().token_usage
kv_metrics.gpu_prefix_cache_hit_rate = self.get_stats().cache_hit_rate
kv_metrics.data_parallel_rank = (
self.ps.dp_rank if self.ps.dp_rank is not None else 0
get_parallel().dp_rank if get_parallel().dp_rank is not None else 0
)
if not self.send_metrics_from_scheduler.closed:
@@ -14,7 +14,7 @@ from sglang.srt.managers.load_snapshot import (
QueueMetrics,
SpeculativeMetrics,
)
from sglang.srt.runtime_context import get_lora
from sglang.srt.runtime_context import get_lora, get_parallel
if TYPE_CHECKING:
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
@@ -215,7 +215,9 @@ class SchedulerLoadInquirer:
decode_moments = list(totals) if totals[0] > 0 else None
return LoadSnapshot(
dp_rank=int(self.ps.dp_rank) if self.ps.dp_rank is not None else 0,
dp_rank=int(get_parallel().dp_rank)
if get_parallel().dp_rank is not None
else 0,
timestamp=time.time(),
num_running_reqs=num_running_reqs,
num_waiting_reqs=num_waiting_reqs,
@@ -309,7 +309,7 @@ class SchedulerMetricsReporter:
if (
get_observability().enable_forward_pass_metrics
and self.scheduler.ps.attn_tp_rank == 0
and self.scheduler.ps.pp_rank == self.scheduler.ps.pp_size - 1
and get_parallel().pp_rank == get_parallel().pp_size - 1
):
from sglang.srt.observability.forward_pass_metrics import (
_FpmPublisherThread,
@@ -32,7 +32,7 @@ from sglang.srt.managers.schedule_batch import (
Req,
)
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
from sglang.srt.runtime_context import get_observability, get_serving
from sglang.srt.runtime_context import get_observability, get_parallel, get_serving
from sglang.srt.server_args import ServerArgs
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.utils.weight_versions import compute_weight_version_spans
@@ -205,7 +205,7 @@ class SchedulerOutputStreamer:
# Send to detokenizer
payload = acc.to_payload(
dp_rank=self.ps.dp_rank,
dp_rank=get_parallel().dp_rank,
is_idle_batch=is_idle_batch,
)
if payload is not None:
@@ -232,7 +232,7 @@ class SchedulerOutputStreamer:
def _maybe_log_time_stats(self, *, req: Req) -> None:
if (
req.finished()
and self.ps.attn_tp_rank == 0
and get_parallel().attn_tp_rank == 0
and get_observability().enable_request_time_stats_logging
):
req.log_time_stats()
@@ -21,7 +21,7 @@ from sglang.srt.managers.io_struct import ProfileReq, ProfileReqOutput, ProfileR
from sglang.srt.model_executor.forward_batch_info import ForwardMode
from sglang.srt.model_executor.step_span_utils import set_detailed_annotations_enabled
from sglang.srt.platforms import current_platform
from sglang.srt.runtime_context import get_device
from sglang.srt.runtime_context import get_device, get_parallel
from sglang.srt.utils import is_mps, is_npu
from sglang.srt.utils.profile_merger import ProfileMerger
from sglang.srt.utils.profile_utils import ProfileManager
@@ -207,10 +207,13 @@ class SchedulerProfilerManager:
self.rpd_profile_path = os.path.join(
self.torch_profiler_output_dir,
"rpd-" + str(time.time()) + f"-TP-{self.ps.tp_rank}" + ".trace.json.gz",
"rpd-"
+ str(time.time())
+ f"-TP-{get_parallel().tp_rank}"
+ ".trace.json.gz",
)
if self.ps.tp_rank == 0:
if get_parallel().tp_rank == 0:
import sqlite3
from rocpd.schema import RocpdSchema
@@ -282,13 +285,13 @@ class SchedulerProfilerManager:
if not self.merge_profiles:
return ""
if self.ps.tp_rank != 0:
if get_parallel().tp_rank != 0:
return ""
if self.ps.dp_size > 1 and self.ps.dp_rank != 0:
if self.ps.dp_size > 1 and get_parallel().dp_rank != 0:
return ""
if self.ps.pp_size > 1 and self.ps.pp_rank != 0:
if get_parallel().pp_size > 1 and get_parallel().pp_rank != 0:
return ""
if self.ps.moe_ep_size > 1 and self.ps.moe_ep_rank != 0:
if get_parallel().moe_ep_size > 1 and get_parallel().moe_ep_rank != 0:
return ""
try:
@@ -336,15 +339,15 @@ class SchedulerProfilerManager:
self.torch_profiler.stop()
if not _is_npu:
# Build filename with only non-zero ranks to maintain backward compatibility
filename_parts = [self.profile_id, f"TP-{self.ps.tp_rank}"]
filename_parts = [self.profile_id, f"TP-{get_parallel().tp_rank}"]
# Only add other ranks if parallelism is enabled (size > 1)
if self.ps.dp_size > 1:
filename_parts.append(f"DP-{self.ps.dp_rank}")
if self.ps.pp_size > 1:
filename_parts.append(f"PP-{self.ps.pp_rank}")
if self.ps.moe_ep_size > 1:
filename_parts.append(f"EP-{self.ps.moe_ep_rank}")
filename_parts.append(f"DP-{get_parallel().dp_rank}")
if get_parallel().pp_size > 1:
filename_parts.append(f"PP-{get_parallel().pp_rank}")
if get_parallel().moe_ep_size > 1:
filename_parts.append(f"EP-{get_parallel().moe_ep_rank}")
filename = (
stage_prefix
@@ -364,7 +367,7 @@ class SchedulerProfilerManager:
self.rpd_profiler.flush()
torch.distributed.barrier(self.dp_tp_cpu_group)
if self.ps.tp_rank == 0:
if get_parallel().tp_rank == 0:
from sglang.srt.utils.rpd_utils import rpd_to_chrome_trace
rpd_to_chrome_trace("trace.rpd", self.rpd_profile_path)
@@ -376,7 +379,7 @@ class SchedulerProfilerManager:
self.torch_profiler_output_dir,
stage_prefix
+ str(time.time())
+ f"-TP-{self.ps.tp_rank}-memory"
+ f"-TP-{get_parallel().tp_rank}-memory"
+ stage_suffix
+ ".pickle",
)
@@ -109,7 +109,7 @@ class SchedulerRequestReceiver:
recv_reqs = self._broadcast_reqs_across_ranks(recv_reqs, local_reqs)
if self.ps.pp_rank == 0:
if get_parallel().pp_rank == 0:
self.unwrap_pickle_wrapper(recv_reqs)
recv_reqs = self._apply_mm_receiver(recv_reqs)
@@ -119,7 +119,7 @@ class SchedulerRequestReceiver:
return recv_reqs
def _pull_raw_reqs(self) -> Optional[List]:
if self.ps.pp_rank == 0:
if get_parallel().pp_rank == 0:
if self.ps.attn_tp_rank == 0 and self.ps.attn_cp_rank == 0:
recv_reqs = []
@@ -158,10 +158,10 @@ class SchedulerRequestReceiver:
)
recv_reqs = point_to_point_pyobj(
[],
self.ps.pp_rank * self.ps.tp_size + dp_offset,
get_parallel().pp_rank * self.ps.tp_size + dp_offset,
self.world_group.cpu_group,
(self.ps.pp_rank - 1) * self.ps.tp_size + dp_offset,
self.ps.pp_rank * self.ps.tp_size + dp_offset,
(get_parallel().pp_rank - 1) * self.ps.tp_size + dp_offset,
get_parallel().pp_rank * self.ps.tp_size + dp_offset,
)
else:
recv_reqs = None
@@ -232,7 +232,7 @@ class SchedulerRequestReceiver:
def _apply_mm_receiver(self, recv_reqs: List) -> List:
# Process MM requests under EPD-disaggregation mode
if (
self.ps.pp_rank == 0
get_parallel().pp_rank == 0
and get_disagg().language_only
and get_disagg().encoder_transfer_backend
in ["zmq_to_scheduler", "mooncake"]
@@ -103,7 +103,9 @@ class SchedulerPPMixin:
for mb_id in range(self.pp_loop_size):
self.running_batch = self.running_mbs[mb_id]
self.last_batch = self.last_mbs[mb_id]
next_first_rank_mb_id = (mb_id + self.ps.pp_size) % self.pp_loop_size
next_first_rank_mb_id = (
mb_id + get_parallel().pp_size
) % self.pp_loop_size
next_mb_id = (mb_id + 1) % self.pp_loop_size
with torch.profiler.record_function("recv_requests"):
recv_reqs = self.ingest_requests()
@@ -243,7 +245,9 @@ class SchedulerPPMixin:
for mb_id in range(self.pp_loop_size):
self.running_batch = self.running_mbs[mb_id]
self.last_batch = self.last_mbs[mb_id]
next_first_rank_mb_id = (mb_id + self.ps.pp_size) % self.pp_loop_size
next_first_rank_mb_id = (
mb_id + get_parallel().pp_size
) % self.pp_loop_size
next_mb_id = (mb_id + 1) % self.pp_loop_size
next_pp_outputs = None
@@ -391,7 +395,9 @@ class SchedulerPPMixin:
for mb_id in range(self.pp_loop_size):
self.running_batch = self.running_mbs[mb_id]
self.last_batch = self.last_mbs[mb_id]
next_first_rank_mb_id = (mb_id + self.ps.pp_size) % self.pp_loop_size
next_first_rank_mb_id = (
mb_id + get_parallel().pp_size
) % self.pp_loop_size
next_mb_id = (mb_id + 1) % self.pp_loop_size
next_pp_outputs = None
@@ -561,7 +567,9 @@ class SchedulerPPMixin:
self.on_idle()
def init_pp_loop_state(self: Scheduler):
self.pp_loop_size: int = self.ps.pp_size + get_parallel().pp_async_batch_depth
self.pp_loop_size: int = (
get_parallel().pp_size + get_parallel().pp_async_batch_depth
)
self.mbs = [None] * self.pp_loop_size
self.last_mbs = [None] * self.pp_loop_size
self.running_mbs = [
@@ -573,7 +581,7 @@ class SchedulerPPMixin:
self.last_rank_comm_queue: deque[Tuple[torch.Event, PPProxyTensors]] = deque()
self._pp_spec_relay = (
envs.SGLANG_ENABLE_PP_SPEC.get()
and self.ps.pp_size > 1
and get_parallel().pp_size > 1
and not self.spec_algorithm.is_none()
)
@@ -805,31 +813,39 @@ class SchedulerPPMixin:
def _pp_send_pyobj_to_next_stage(self: Scheduler, data, async_send: bool = False):
p2p_work = []
if self.ps.attn_tp_rank == 0 and self.ps.attn_cp_rank == 0:
if get_parallel().attn_tp_rank == 0 and get_parallel().attn_cp_rank == 0:
dp_offset = (
self.ps.attn_dp_rank * self.ps.attn_cp_size * self.ps.attn_tp_size
self.ps.attn_dp_rank
* get_parallel().attn_cp_size
* get_parallel().attn_tp_size
)
p2p_work = point_to_point_pyobj(
data,
self.ps.pp_rank * self.ps.tp_size + dp_offset,
get_parallel().pp_rank * get_parallel().tp_size + dp_offset,
self.world_group.cpu_group,
self.ps.pp_rank * self.ps.tp_size + dp_offset,
((self.ps.pp_rank + 1) % self.ps.pp_size) * self.ps.tp_size + dp_offset,
get_parallel().pp_rank * get_parallel().tp_size + dp_offset,
((get_parallel().pp_rank + 1) % get_parallel().pp_size)
* get_parallel().tp_size
+ dp_offset,
async_send=async_send,
)
return p2p_work
def _pp_recv_pyobj_from_prev_stage(self: Scheduler):
if self.ps.attn_tp_rank == 0 and self.ps.attn_cp_rank == 0:
if get_parallel().attn_tp_rank == 0 and get_parallel().attn_cp_rank == 0:
dp_offset = (
self.ps.attn_dp_rank * self.ps.attn_cp_size * self.ps.attn_tp_size
self.ps.attn_dp_rank
* get_parallel().attn_cp_size
* get_parallel().attn_tp_size
)
data = point_to_point_pyobj(
[],
self.ps.pp_rank * self.ps.tp_size + dp_offset,
get_parallel().pp_rank * get_parallel().tp_size + dp_offset,
self.world_group.cpu_group,
((self.ps.pp_rank - 1) % self.ps.pp_size) * self.ps.tp_size + dp_offset,
self.ps.pp_rank * self.ps.tp_size + dp_offset,
((get_parallel().pp_rank - 1) % get_parallel().pp_size)
* get_parallel().tp_size
+ dp_offset,
get_parallel().pp_rank * get_parallel().tp_size + dp_offset,
)
else:
data = None
@@ -1470,7 +1486,7 @@ class SchedulerPPMixin:
# posted, so the parity-based send-first/recv-first ordering used
# for NPU is replaced by batch_isend_irecv which submits all
# send/recv operations atomically.
if _is_npu and self.ps.pp_size == 2:
if _is_npu and get_parallel().pp_size == 2:
return self._pp2_only_send_recv_output_tensors_npu(
next_first_rank_mb_id,
next_mb_id,
@@ -1498,7 +1514,7 @@ class SchedulerPPMixin:
# makes rank 1 post its recv first, which breaks the cycle for any
# pp_size > 1.
needs_pairing = is_xpu() or self._pp_spec_relay
send_first = (not needs_pairing) or ((self.ps.pp_rank % 2) == 0)
send_first = (not needs_pairing) or ((get_parallel().pp_rank % 2) == 0)
def _do_send():
return self._pp_send_output_to_next_stage(
+5 -3
View File
@@ -418,7 +418,7 @@ class TpModelWorker(BaseTpWorker):
else:
self.random_seed = broadcast_pyobj(
[get_device().random_seed],
self.ps.tp_size * self.ps.pp_rank + self.ps.tp_rank,
self.ps.tp_size * get_parallel().pp_rank + self.ps.tp_rank,
self.world_group.cpu_group,
src=self.world_group.ranks[0],
)[0]
@@ -451,7 +451,8 @@ class TpModelWorker(BaseTpWorker):
assert self.model_runner.max_running_requests > 0, "max_running_request is zero"
max_req_len = min(
self.model_config.context_len - 1,
self.model_runner.effective_max_total_num_tokens * self.ps.attn_dcp_size
self.model_runner.effective_max_total_num_tokens
* get_parallel().attn_dcp_size
- 1,
)
assert max_req_len > 0, "Memory pool size is too small"
@@ -578,7 +579,8 @@ class TpModelWorker(BaseTpWorker):
def get_worker_info(self):
max_req_len = min(
self.model_config.context_len - 1,
self.model_runner.effective_max_total_num_tokens * self.ps.attn_dcp_size
self.model_runner.effective_max_total_num_tokens
* get_parallel().attn_dcp_size
- 1,
)
return (
@@ -2420,7 +2420,9 @@ class KVCacheConfigurator:
sum(1 for i in all_mamba_layers if start <= i < end)
for start, end in (
get_pp_indices(
self.model_config.num_hidden_layers, rank, self.ps.pp_size
self.model_config.num_hidden_layers,
rank,
self.ps.pp_size,
)
for rank in range(self.ps.pp_size)
)
@@ -445,7 +445,7 @@ class ModelRunner:
self.prefill_shared_read_stager: Optional[Callable[[ForwardBatch], bool]] = None
# CPU offload
set_offloader(create_offloader(dp_rank=self.ps.dp_rank))
set_offloader(create_offloader(dp_rank=get_parallel().dp_rank))
self._weight_checker = WeightChecker(get_model=lambda: self.model, ps=self.ps)
@@ -481,7 +481,7 @@ class ModelRunner:
"pp_proxy_tensors" in inspect.signature(self.model.forward).parameters
)
if self.ps.pp_size > 1:
if get_parallel().pp_size > 1:
if not (envs.SGLANG_ENABLE_PP_SPEC.get() and self.is_draft_worker):
assert self.support_pp, (
"Pipeline Parallel is not compatible with this model."
@@ -647,8 +647,8 @@ class ModelRunner:
from sglang.srt.model_executor.mindspore_runner import init_ms_distributed
init_ms_distributed(
world_size=self.ps.tp_size * self.ps.pp_size,
rank=self.ps.tp_size * self.ps.pp_rank + self.ps.tp_rank,
world_size=self.ps.tp_size * get_parallel().pp_size,
rank=self.ps.tp_size * get_parallel().pp_rank + self.ps.tp_rank,
local_rank=self.gpu_id,
port=self.dist_port,
)
@@ -667,8 +667,8 @@ class ModelRunner:
prepare_moe_topk(
model=self.model,
model_config=self.model_config,
moe_ep_size=self.ps.moe_ep_size,
moe_ep_rank=self.ps.moe_ep_rank,
moe_ep_size=get_parallel().moe_ep_size,
moe_ep_rank=get_parallel().moe_ep_rank,
)
self.maybe_init_dwdp()
@@ -708,7 +708,7 @@ class ModelRunner:
def maybe_init_expert_location_metadata(self):
if self.is_draft_worker:
return
expert_rank = self.ps.moe_ep_rank + (
expert_rank = get_parallel().moe_ep_rank + (
get_parallel().ep_join_rank_offset
if get_exec().moe.is_ep_scale_joiner
else 0
@@ -767,8 +767,8 @@ class ModelRunner:
self.expert_backup_client = (
ExpertBackupClient(
model_config=self.model_config,
moe_ep_size=self.ps.moe_ep_size,
moe_ep_rank=self.ps.moe_ep_rank,
moe_ep_size=get_parallel().moe_ep_size,
moe_ep_rank=get_parallel().moe_ep_rank,
get_model=lambda: self.model,
)
if (
@@ -803,16 +803,16 @@ class ModelRunner:
def get_pp_proxy_topk_size(self) -> Optional[int]:
return misc_utils.resolve_pp_proxy_topk_size(
model_config=self.model_config,
pp_size=self.ps.pp_size,
pp_rank=self.ps.pp_rank,
pp_size=get_parallel().pp_size,
pp_rank=get_parallel().pp_rank,
start_layer=self.layer_info.start_layer,
)
def get_pp_proxy_residual_num_blocks(self) -> Optional[int]:
return misc_utils.resolve_pp_proxy_residual_num_blocks(
model_config=self.model_config,
pp_size=self.ps.pp_size,
pp_rank=self.ps.pp_rank,
pp_size=get_parallel().pp_size,
pp_rank=get_parallel().pp_rank,
start_layer=self.layer_info.start_layer,
)
@@ -980,7 +980,7 @@ class ModelRunner:
swap_in_block_size=hisparse_cfg.swap_in_block_size,
shared_index_layers=resolve_shared_index_layers(
hf_text_config=self.model_config.hf_text_config,
pp_size=self.ps.pp_size,
pp_size=get_parallel().pp_size,
is_speculative=self.spec_algorithm.is_speculative(),
),
)
@@ -1157,8 +1157,8 @@ class ModelRunner:
check_quantized_moe_compatibility(
model_config=self.model_config,
tp_size=self.ps.tp_size,
moe_ep_size=self.ps.moe_ep_size,
moe_dp_size=self.ps.moe_dp_size,
moe_ep_size=get_parallel().moe_ep_size,
moe_dp_size=get_parallel().moe_dp_size,
)
def init_torch_distributed(self):
@@ -1293,7 +1293,7 @@ class ModelRunner:
is_draft_worker=self.is_draft_worker,
tp_size=self.ps.tp_size,
tp_rank=self.ps.tp_rank,
pp_rank=self.ps.pp_rank,
pp_rank=get_parallel().pp_rank,
)
if dumper.may_enable:
@@ -1580,7 +1580,7 @@ class ModelRunner:
dp_size = 1 if get_parallel().enable_dp_attention else self.ps.dp_size
self.local_omp_cpuid = numa_utils.init_threads_binding(
numa_index=self.gpu_id,
world_size=dp_size * self.ps.tp_size * self.ps.pp_size,
world_size=dp_size * self.ps.tp_size * get_parallel().pp_size,
)
def apply_torch_tp(self):
@@ -274,7 +274,7 @@ class BaseRunner(ABC):
if (
envs.SGLANG_PP_PARALLEL_DEEPGEMM_WARMUP.get()
and deep_gemm_wrapper.ENABLE_JIT_DEEPGEMM
and mr.ps.pp_size > 1
and get_parallel().pp_size > 1
and not mr.spec_algorithm.is_speculative()
):
from sglang.srt.layers.deep_gemm_wrapper.compile_utils import (
@@ -546,7 +546,7 @@ class BaseRunner(ABC):
pp_hidden_tokens = num_tokens
if (
capture_forward_mode == ForwardMode.EXTEND
and mr.ps.pp_rank != 0
and get_parallel().pp_rank != 0
and mr.ps.attn_cp_size > 1
):
pp_hidden_tokens = num_tokens // mr.ps.attn_cp_size
@@ -30,6 +30,7 @@ from sglang.srt.runtime_context import (
get_disagg,
get_exec,
get_model,
get_parallel,
get_schedule,
get_spec,
max_prefill_buffer_tokens,
@@ -147,7 +148,7 @@ def flashinfer_autotune_cache_path(model_runner: ModelRunner) -> Path:
str(get_model().quantization),
str(get_exec().moe.moe_runner_backend),
str(mr.ps.tp_size),
str(mr.ps.pp_size),
str(get_parallel().pp_size),
str(mr.ps.attn_dp_size),
str(mr.ps.moe_ep_size),
str(mr.model_config.hf_config.__class__.__name__),
@@ -170,7 +171,7 @@ def flashinfer_autotune_cache_path(model_runner: ModelRunner) -> Path:
cache_dir.mkdir(parents=True, exist_ok=True)
return (
cache_dir
/ f"rank_tp{mr.ps.tp_rank}_pp{mr.ps.pp_rank}_dp{mr.ps.dp_rank or 0}.json"
/ f"rank_tp{mr.ps.tp_rank}_pp{get_parallel().pp_rank}_dp{mr.ps.dp_rank or 0}.json"
)
@@ -34,6 +34,7 @@ from sglang.srt.runtime_context import (
get_context,
get_disagg,
get_observability,
get_parallel,
get_schedule,
get_serving,
)
@@ -1118,7 +1119,7 @@ class SchedulerMetricsCollector(_StatLoggerDIMixin):
)
enable_kv_cache_events = bool(
get_observability().kv_events_config
and ps.pp_rank == 0
and get_parallel().pp_rank == 0
and ps.attn_tp_rank == 0
and ps.attn_cp_rank == 0
)
+19 -4
View File
@@ -21,7 +21,7 @@ from typing import Any, Dict, Optional
import ray
from sglang.srt.arg_groups.overrides import declare_resolution
from sglang.srt.runtime_context import publish
from sglang.srt.runtime_context import SpawnRanks, publish, spawn_world_rank
from sglang.srt.server_args import PortArgs, ServerArgs
logger = logging.getLogger(__name__)
@@ -49,7 +49,11 @@ class SchedulerActor:
dist_init_addr: Optional[str] = None,
):
from sglang.srt.environ import envs
from sglang.srt.managers.scheduler import Scheduler, configure_scheduler_process
from sglang.srt.managers.scheduler import (
Scheduler,
configure_scheduler_process,
resolve_spawn_dp_rank,
)
from sglang.srt.utils.numa_utils import (
get_numa_node_if_available,
numa_bind_to_node,
@@ -77,12 +81,23 @@ class SchedulerActor:
actual_gpu_id = gpu_id
logger.info(f"[TP{tp_rank}] Using passed gpu_id: {gpu_id}")
dp_rank = resolve_spawn_dp_rank(dp_rank)
# This actor takes the place of run_scheduler_process, which is where
# a forked scheduler publishes.
publish(server_args, role="scheduler")
publish(
server_args,
role="scheduler",
ranks=SpawnRanks(
world_rank=spawn_world_rank(
server_args, tp_rank=tp_rank, pp_rank=pp_rank
),
dp_rank=dp_rank,
),
)
# Configure worker (logging, process title, etc.)
dp_rank = configure_scheduler_process(
configure_scheduler_process(
server_args,
actual_gpu_id,
tp_rank,
+199 -6
View File
@@ -132,6 +132,10 @@ class Live(msgspec.Struct, frozen=True):
source: Any = None
doc: str = ""
# For a stamp-only name (`source=None`): what a reader should be told when
# nothing has stamped it. These names have no fallback by construction, so
# the message is the only thing pointing at what did not happen.
unstamped: str = ""
_LIVE_READS: dict = {
@@ -177,7 +181,35 @@ _LIVE_READS: dict = {
"attn_cp_rank": "get_attn_context_model_parallel_rank",
"dcp_rank": "get_dcp_rank",
"attn_dcp_rank": lambda self: self.dcp_rank if self.dcp_enabled else 0,
"attn_dp_rank": None,
"attn_dp_rank": Live(
source=None,
doc=(
"This process's index in the attention-DP group. Computed from "
"`tp_rank` when the attention topology is initialized, and moved "
"by an elastic scale-up, so no coordinator can answer it."
),
unstamped=(
"it is computed from this process's `tp_rank` when the attention "
"topology is initialized, so a process that never ran "
"`initialize_dp_attention` has no answer to give"
),
),
"dp_rank": Live(
source=None,
doc=(
"Which data-parallel replica this process serves, as the data "
"parallel controller numbered them at spawn. `None` when there is "
"no controller. Unlike `attn_dp_rank` and `moe_dp_rank` it is not "
"a position in any process group -- no group has one member per "
"replica -- which is why nothing can derive it and the spawn "
"states it instead."
),
unstamped=(
"it is a spawn identity, handed to `publish(..., ranks=...)` by "
"the process entry; a process that published without a rank "
"bundle has no replica index to report"
),
),
"world_group": "get_world_group",
"tp_group": "get_tp_group",
"pp_group": "get_pp_group",
@@ -227,6 +259,80 @@ def derive_attention_widths(
return attn_dp_size, tp_size // attn_dp_size // attn_cp_size
def derive_attention_ranks(
*, tp_rank: int, attn_tp_size: int, attn_cp_size: int, enable_dp_attention: bool
) -> tuple:
"""(attn_tp_rank, attn_dp_rank) for a process at `tp_rank`.
The rank layout is (dp, cp, tp) with tp the fastest-changing dimension::
tp_rank = (attn_dp_rank * attn_cp_size + attn_cp_rank) * attn_tp_size
+ attn_tp_rank
Split out beside `derive_attention_widths` because two places need it from
different inputs: `publish` has this process's `tp_rank` from the spawn and
the widths from the configuration, while `initialize_dp_attention` has them
from the groups it just built. They must not carry separate copies of the
arithmetic -- the point of computing it at publish is that the two agree.
"""
attn_tp_rank = tp_rank % attn_tp_size
if not enable_dp_attention:
return attn_tp_rank, 0
return attn_tp_rank, tp_rank // (attn_tp_size * attn_cp_size)
def spawn_world_rank(server_args, *, tp_rank: int, pp_rank: int) -> int:
"""This process's place in WORLD, from the ranks its entry was given.
The inverse of `derive_spawn_ranks`, for the entries that have the pieces
but not the whole: the same expression `bootstrap` hands to
`init_distributed_environment` when it builds the group.
Reads a resolving view because it runs before `publish`.
"""
from sglang.srt.arg_groups.model_override_base import resolving_view
cfg = resolving_view(server_args)
return cfg.ep_join_rank_offset + cfg.tp_size * pp_rank + tp_rank
def derive_spawn_ranks(
*,
world_rank: int,
tp_size: int,
ep_join_rank_offset: int,
attn_cp_size: int,
attn_tp_size: int,
moe_dp_size: int,
moe_ep_size: int,
) -> dict:
"""Every rank a process group would answer, from its place in WORLD.
WORLD is laid out `rank = ep_join_rank_offset + tp_size * pp_rank +
tp_rank`: `initialize_model_parallel` builds tensor-parallel groups as
contiguous blocks of `tp_size` and pipeline groups strided by it, so the
map is a bijection and this is its inverse. The attention and MoE ranks
are then positions inside the tensor-parallel block, which is what the
launcher computes when it decides what to spawn.
Pure arithmetic over the published widths: no group is consulted, which is
the point -- this runs at publish, before any of them exist.
"""
local = world_rank - ep_join_rank_offset
tp_rank = local % tp_size
return {
"tp_rank": tp_rank,
"pp_rank": local // tp_size,
"attn_cp_rank": (tp_rank // attn_tp_size) % attn_cp_size,
"moe_dp_rank": tp_rank // (tp_size // moe_dp_size),
"moe_ep_rank": (
tp_rank
% (tp_size // moe_dp_size)
// (tp_size // moe_dp_size // moe_ep_size)
),
}
def derive_parallel_widths(
*,
tp_size: int,
@@ -327,6 +433,31 @@ def dcp_enabled_of(cfg: Any):
return parallel_widths_of(cfg)["dcp_enabled"]
class SpawnRanks(msgspec.Struct, frozen=True):
"""Where the launcher put this process, in the two numbers only it knows.
`world_rank` is this process's place in the WORLD group, which fixes every
other rank: the groups are laid out from the published widths, so
`tp_rank`, `pp_rank` and the attention / MoE ranks are functions of it (see
`derive_spawn_ranks`). Passing them separately would be passing the same
fact five more times, with five more ways for an entry to contradict
itself.
`dp_rank` is the exception, because data-parallel replicas are separate
WORLD groups: with `--dp-size 2` each replica holds ranks `0 .. n-1`, so
the rank cannot say which replica this is. `None` means "no controller",
which is an answer rather than an absence, and it is recorded as one.
Nothing else belongs here. A device index, for instance, is a placement
decision rather than a position -- the launcher may reindex it, and Ray
assigns it from its own allocator -- so it stays an argument to whoever
was handed it.
"""
world_rank: int
dp_rank: Optional[int] = None
class ParallelContext:
"""Parallel-topology namespace: one spelling per name.
@@ -394,11 +525,10 @@ class ParallelContext:
return getattr(_ps(), source)()
if source is not None:
return source(self)
why = live.unstamped if isinstance(live, Live) else ""
raise RuntimeError(
f"parallel rank {name!r} is not available: it is computed from "
"this process's `tp_rank` when the attention topology is "
"initialized, so a process that never ran "
"`initialize_dp_attention` has no answer to give"
f"parallel name {name!r} is not available: "
+ (why or "nothing has stamped it in this process")
)
if config is None and name in _parallel_config_leaves():
raise ValueError("config namespace 'parallel' not published")
@@ -1598,7 +1728,13 @@ def _dump_recorded_namespace_reads() -> None:
)
def publish(server_args, *, role: str, hf_config: Any = None) -> RuntimeContext:
def publish(
server_args,
*,
role: str,
hf_config: Any = None,
ranks: SpawnRanks | None = None,
) -> RuntimeContext:
"""Install process-wide config for this OS process.
Records the process ``role`` — one of the ``ROLE_NAMESPACE_SETS`` keys,
@@ -1609,6 +1745,12 @@ def publish(server_args, *, role: str, hf_config: Any = None) -> RuntimeContext:
namespace-read enforcement (``record`` audits the reads instead).
``hf_config`` is accepted for forward-compat and currently unused.
``ranks`` is who this process is, from the entry that spawned it. It is
optional because most roles are not placed in the topology at all -- a
tokenizer has no ``tp_rank`` -- and those processes raise on a rank read
exactly as they do today, with a message naming the missing bundle rather
than an absent process group.
A process holds at most one live config: the bags always describe the
engine running now. Re-publish is allowed and is **last-publish-wins**
(bags re-projected, provenance reset, role overwritten), which is what
@@ -1634,6 +1776,36 @@ def publish(server_args, *, role: str, hf_config: Any = None) -> RuntimeContext:
),
)
_CONTEXT._publish_role = role
if ranks is not None:
# The placement, worked out here rather than carried: the widths are on
# the bag a moment ago, and `world_rank` fixes the rest. A read of any
# of these then needs no process group, which is the point -- they are
# read long before one exists. The scoped overrides that swap a group
# for a draft worker sit above the record in the read chain, so a
# scope still wins.
parallel = _CONTEXT.parallel
placement = derive_spawn_ranks(
world_rank=ranks.world_rank,
tp_size=parallel.tp_size,
ep_join_rank_offset=parallel.ep_join_rank_offset,
attn_cp_size=parallel.attn_cp_size,
attn_tp_size=parallel.attn_tp_size,
moe_dp_size=parallel.moe_dp_size,
moe_ep_size=parallel.moe_ep_size,
)
# `moe_dp_rank` is a different quantity when the MoE-DP group is
# aliased to the attention-CP one: the group answers the CP index,
# while this computes the MoE-DP index. Leave it to the group there, so
# one name does not mean two things.
if parallel.moe_dp_size < parallel.attn_cp_size:
placement.pop("moe_dp_rank")
# `dp_rank` is recorded whatever it is, None included: replicas are
# separate WORLD groups, so no rank implies it and `None` is the answer
# "no controller" rather than an absence.
placement["dp_rank"] = ranks.dp_rank
placement["launch_world_rank"] = ranks.world_rank
parallel.override_permanently(**placement)
_stamp_attention_ranks(parallel, placement["tp_rank"])
if _ROLE_NS_MODE == "record":
# The '-' marker distinguishes a zero-read role from a process where
# recording never ran (signal teardown skips atexit).
@@ -1648,6 +1820,27 @@ def publish(server_args, *, role: str, hf_config: Any = None) -> RuntimeContext:
return _CONTEXT
def _stamp_attention_ranks(parallel, tp_rank: int) -> None:
"""Place this process in the attention topology, from the configuration.
The widths are already on the bag -- `publish` computed them a moment ago --
and the rank comes from the spawn, so the position is known here, before any
process group exists. That is the point: a rank read then works in a process
that never initialises distributed, which is what `ParallelState` provided
by being a plain frozen record.
It is a stamp rather than a bag leaf because it is a per-process fact, and
nothing about the configuration distinguishes one rank from another.
"""
attn_tp_rank, attn_dp_rank = derive_attention_ranks(
tp_rank=tp_rank,
attn_tp_size=parallel.attn_tp_size,
attn_cp_size=parallel.attn_cp_size,
enable_dp_attention=parallel.enable_dp_attention,
)
parallel.override_permanently(attn_tp_rank=attn_tp_rank, attn_dp_rank=attn_dp_rank)
def assert_published(server_args, *, role: str) -> RuntimeContext:
"""This record, under this role, is already published -- or fail loud.
+1 -1
View File
@@ -124,7 +124,7 @@ class RustServer:
# The joining TP group is entirely local to this node.
tp_size_per_node = scheduler.ps.tp_size
else:
nnodes_per_pp_rank = max(get_parallel().nnodes // scheduler.ps.pp_size, 1)
nnodes_per_pp_rank = max(get_parallel().nnodes // get_parallel().pp_size, 1)
tp_size_per_node = scheduler.ps.tp_size // nnodes_per_pp_rank
dp_group_width = scheduler.ps.attn_tp_size * scheduler.ps.attn_cp_size
# Count DP leaders within this node's TP range. The first leader must
@@ -84,6 +84,7 @@ from sglang.srt.speculative.spec_utils import (
GrammarTree,
assign_req_to_token_pool_func,
build_grammar_vocab_mask,
draft_pp_context,
draft_tp_context,
)
from sglang.srt.utils import is_cuda, is_hip, is_npu, is_xpu
@@ -402,7 +403,7 @@ class DFlashWorkerV2(BaseSpecWorker):
draft_init_ctx = draft_tp_context(get_parallel().attn_tp_group)
else:
draft_init_ctx = empty_context()
with draft_init_ctx:
with draft_pp_context(), draft_init_ctx:
bundle = build_draft_tp_worker(
server_args=server_args,
gpu_id=gpu_id,
@@ -598,7 +599,10 @@ class DFlashWorkerV2(BaseSpecWorker):
)
def init_attention_backends(self):
with self.draft_tp_context(self.draft_model_runner.tp_group):
with (
draft_pp_context(),
self.draft_tp_context(self.draft_model_runner.tp_group),
):
self._draft_worker.init_attention_backends()
self._need_mamba_verify_commit = mambaish_config(
self.model_runner.model_config
@@ -608,7 +612,10 @@ class DFlashWorkerV2(BaseSpecWorker):
)
def init_cuda_graphs(self):
with self.draft_tp_context(self.draft_model_runner.tp_group):
with (
draft_pp_context(),
self.draft_tp_context(self.draft_model_runner.tp_group),
):
capture_decode_cuda_graph = (
get_exec().graph.cuda_graph_config.decode.backend != Backend.DISABLED
)
@@ -74,6 +74,7 @@ from sglang.srt.speculative.spec_tp_sync import SpecTpSync, SpecTpSyncSite
from sglang.srt.speculative.spec_utils import (
GrammarTree,
build_grammar_vocab_mask,
draft_pp_context,
draft_tp_context,
prepare_mamba_track_for_verify,
)
@@ -165,7 +166,7 @@ class DSparkWorkerV2(BaseSpecWorker):
"MoE-under-DP all-reduce."
)
with self._draft_context():
with draft_pp_context(), self._draft_context():
bundle = build_draft_tp_worker(
server_args=server_args,
gpu_id=gpu_id,
@@ -428,7 +429,7 @@ class DSparkWorkerV2(BaseSpecWorker):
)
def init_attention_backends(self):
with self._draft_context():
with draft_pp_context(), self._draft_context():
self._draft_worker.init_attention_backends()
self._target_hidden_projection_enabled = _configure_target_hidden_projection(
target_model=self.target_worker.model_runner.model,
@@ -463,7 +464,7 @@ class DSparkWorkerV2(BaseSpecWorker):
"memory is available after target backend initialization.",
available_mem,
)
with self._draft_context():
with draft_pp_context(), self._draft_context():
if capture_decode_cuda_graph:
# Keep the draft model graph enabled when folded proposal is
# disabled, but do not capture the proposal head as a tail
@@ -338,6 +338,7 @@ class EagleDraftWorker(EagleDraftWorkerBase):
def init_attention_backends(self):
with (
draft_pp_context(),
self.draft_tp_context(self.draft_runner.tp_group),
speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(),
@@ -347,6 +348,7 @@ class EagleDraftWorker(EagleDraftWorkerBase):
def init_cuda_graphs(self):
with (
draft_pp_context(),
self.draft_tp_context(self.draft_runner.tp_group),
speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(),
@@ -73,6 +73,7 @@ from sglang.srt.speculative.frozen_kv_mtp_utils import (
)
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.speculative.spec_utils import (
draft_pp_context,
draft_tp_context,
fast_topk,
get_plan_stream,
@@ -134,7 +135,7 @@ class FrozenKVMTPDraftWorker(EagleDraftWorkerBase, TpModelWorker):
self.hot_token_id = None
with (
empty_context(),
draft_pp_context(),
speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(),
draft_model_build_scope(),
@@ -204,6 +205,7 @@ class FrozenKVMTPDraftWorker(EagleDraftWorkerBase, TpModelWorker):
def init_attention_backends(self):
with (
draft_pp_context(),
self.draft_tp_context(self.draft_model_runner.tp_group),
speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(),
@@ -214,6 +216,7 @@ class FrozenKVMTPDraftWorker(EagleDraftWorkerBase, TpModelWorker):
def init_cuda_graphs(self):
with (
draft_pp_context(),
self.draft_tp_context(self.draft_model_runner.tp_group),
speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(),
@@ -82,6 +82,7 @@ from sglang.srt.speculative.multi_layer_eagle_utils import (
)
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.speculative.spec_utils import (
draft_pp_context,
draft_tp_context,
get_plan_stream,
sample_draft_proposal,
@@ -163,7 +164,7 @@ class MultiLayerEagleDraftWorker(EagleDraftWorkerBase):
# Load draft model weights only.
with (
empty_context(),
draft_pp_context(),
speculative_moe_backend_context(),
draft_model_build_scope(),
):
@@ -221,6 +222,7 @@ class MultiLayerEagleDraftWorker(EagleDraftWorkerBase):
def init_attention_backends(self):
with (
draft_pp_context(),
self.draft_tp_context(self.draft_runner_list[0].tp_group),
speculative_moe_backend_context(),
):
@@ -228,6 +230,7 @@ class MultiLayerEagleDraftWorker(EagleDraftWorkerBase):
def init_cuda_graphs(self):
with (
draft_pp_context(),
self.draft_tp_context(self.draft_runner_list[0].tp_group),
speculative_moe_backend_context(),
):
@@ -27,7 +27,11 @@ from sglang.srt.speculative.base_spec_worker import (
from sglang.srt.speculative.eagle_utils import default_tree_mask_mode
from sglang.srt.speculative.eagle_worker_v2 import EagleDraftWorker, EAGLEWorkerV2
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.speculative.spec_utils import draft_tp_context, get_plan_stream
from sglang.srt.speculative.spec_utils import (
draft_pp_context,
draft_tp_context,
get_plan_stream,
)
from sglang.srt.utils import empty_context, get_bool_env_var
logger = logging.getLogger(__name__)
@@ -76,7 +80,7 @@ class StandaloneDraftWorker(EagleDraftWorker):
# whose MoE gates run during construction; the scope routes their
# fusion decision to the speculative leaf (it does not swap
# runner_backend — the draft's forwards run outside that context).
with empty_context(), draft_model_build_scope():
with draft_pp_context(), draft_model_build_scope():
self.draft_worker = TpModelWorker(
server_args=server_args,
gpu_id=gpu_id,
+14 -11
View File
@@ -18,7 +18,7 @@ from sglang.srt.model_executor.step_span_utils import (
set_detailed_annotations_enabled,
)
from sglang.srt.platforms import current_platform
from sglang.srt.runtime_context import get_device
from sglang.srt.runtime_context import get_device, get_parallel
from sglang.srt.utils import is_npu
from sglang.srt.utils.torch_npu_patch_utils import apply_torch_npu_patches
@@ -358,15 +358,15 @@ class _ProfilerTorch(_ProfilerConcreteBase):
self.torch_profiler.stop()
if not _is_npu:
# Build filename with only non-zero ranks to maintain backward compatibility
filename_parts = [self.profile_id, f"TP-{self.ps.tp_rank}"]
filename_parts = [self.profile_id, f"TP-{get_parallel().tp_rank}"]
# Only add other ranks if parallelism is enabled (size > 1)
if self.ps.dp_size > 1:
filename_parts.append(f"DP-{self.ps.dp_rank}")
if self.ps.pp_size > 1:
filename_parts.append(f"PP-{self.ps.pp_rank}")
if self.ps.moe_ep_size > 1:
filename_parts.append(f"EP-{self.ps.moe_ep_rank}")
filename_parts.append(f"DP-{get_parallel().dp_rank}")
if get_parallel().pp_size > 1:
filename_parts.append(f"PP-{get_parallel().pp_rank}")
if get_parallel().moe_ep_size > 1:
filename_parts.append(f"EP-{get_parallel().moe_ep_rank}")
filename = (
(self.output_prefix + "-" if self.output_prefix else "")
@@ -396,7 +396,7 @@ class _ProfilerMemory(_ProfilerConcreteBase):
self.output_dir,
(self.output_prefix + "-" if self.output_prefix else "")
+ str(time.time())
+ f"-TP-{self.ps.tp_rank}-memory"
+ f"-TP-{get_parallel().tp_rank}-memory"
+ self.output_suffix
+ ".pickle",
)
@@ -426,10 +426,13 @@ class _ProfilerRPD(_ProfilerConcreteBase):
self.rpd_profile_path = os.path.join(
self.output_dir,
"rpd-" + str(time.time()) + f"-TP-{self.ps.tp_rank}" + ".trace.json.gz",
"rpd-"
+ str(time.time())
+ f"-TP-{get_parallel().tp_rank}"
+ ".trace.json.gz",
)
if self.ps.tp_rank == 0:
if get_parallel().tp_rank == 0:
import sqlite3
from rocpd.schema import RocpdSchema
@@ -454,7 +457,7 @@ class _ProfilerRPD(_ProfilerConcreteBase):
self.rpd_profiler.flush()
torch.distributed.barrier(self.cpu_group)
if self.ps.tp_rank == 0:
if get_parallel().tp_rank == 0:
from sglang.srt.utils.rpd_utils import rpd_to_chrome_trace
rpd_to_chrome_trace("trace.rpd", self.rpd_profile_path)
+13 -1
View File
@@ -51,9 +51,11 @@ from sglang.srt.arg_groups.overrides import resolving_view
from sglang.srt.configs.load_config import LoadConfig
from sglang.srt.platforms import current_platform
from sglang.srt.runtime_context import (
SpawnRanks,
get_exec,
get_parallel,
publish,
spawn_world_rank,
)
from .protocol import (
@@ -281,7 +283,17 @@ class WeightCacheDaemon:
from sglang.srt.model_loader.loader import get_model_loader
server_args = self.server_args
publish(server_args, role="weight_cache_daemon")
# The launcher told this daemon where it sits, and it builds the same
# groups a scheduler does, so the same one number places it.
publish(
server_args,
role="weight_cache_daemon",
ranks=SpawnRanks(
world_rank=spawn_world_rank(
server_args, tp_rank=self.tp_rank, pp_rank=self.pp_rank
)
),
)
from sglang.srt.layers.moe import initialize_moe_config
+42
View File
@@ -2,6 +2,7 @@
import argparse
import asyncio
import contextlib
import copy
import doctest
import importlib.util
@@ -2002,6 +2003,36 @@ def maybe_stub_sgl_kernel():
sys.meta_path.insert(0, _SglKernelFinder())
@contextlib.contextmanager
def published_topology(role: str = "test", *, ranks=None, **server_args_fields):
"""Publish a record describing the parallel topology a test wants.
Replaces standing a per-process parallel record into the object under
test. The widths arrive the way production gets them -- from published
configuration -- and the per-process ranks the way a spawned process gets
them, so a rank read is answered without building a process group. Stating
the topology through the same door production uses also keeps the derived
widths honest: a hand-built double can claim an `attn_tp_size` the
configuration would never produce.
`ranks` overrides the spawn identities; by default this process is rank
zero of the world, which fixes every other rank. The context is reset on exit, including when the
test fails.
"""
from sglang.srt.runtime_context import SpawnRanks, publish, reset_context
from sglang.srt.server_args import ServerArgs
bundle = dict(world_rank=0, dp_rank=None)
bundle.update(ranks or {})
server_args = ServerArgs(model_path="dummy", **server_args_fields)
reset_context()
publish(server_args, role=role, ranks=SpawnRanks(**bundle))
try:
yield server_args
finally:
reset_context()
_GPU_IDLE_TIMEOUT_SECS = 30.0
_GPU_IDLE_POLL_INTERVAL_SECS = 2.0
_GPU_IDLE_USED_MEMORY_THRESHOLD = 2 << 30 # 2 GiB
@@ -2253,6 +2284,17 @@ def enter_override(test_case, override):
return installed
def enter_scope(test_case, scope):
"""Enter a context manager for the length of one test.
The `with`-statement form of `enter_override` above, and 3.10-safe for the
same reason: `enterContext` arrived in 3.11.
"""
entered = scope.__enter__()
test_case.addCleanup(scope.__exit__, None, None, None)
return entered
class CustomTestCase(unittest.TestCase):
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)