State the draft's whole topology in its scope, and read the rest from the context (#40339)

This commit is contained in:
Cheng Wan
2026-09-21 12:19:38 -07:00
committed by GitHub
parent ae7a516ba7
commit 0db1a93adb
22 changed files with 364 additions and 176 deletions
@@ -27,17 +27,15 @@ from abc import ABC, abstractmethod
from collections import deque from collections import deque
from itertools import count from itertools import count
from queue import Queue from queue import Queue
from typing import TYPE_CHECKING, Any, Callable, Optional, Union from typing import Any, Callable, Optional, Union
import msgspec import msgspec
import zmq import zmq
from pydantic import BaseModel from pydantic import BaseModel
from sglang.srt.runtime_context import get_parallel
from sglang.srt.utils.network import NetworkAddress from sglang.srt.utils.network import NetworkAddress
if TYPE_CHECKING:
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -63,17 +61,18 @@ def select_kv_publisher_dp_rank(
return dp_rank or 0 return dp_rank or 0
def is_kv_publisher_rank(kv_events_config: Optional[str], ps: "ParallelState") -> bool: def is_kv_publisher_rank(kv_events_config: Optional[str]) -> bool:
"""Whether this scheduler owns a KV-event publisher slot: one per """Whether this scheduler owns a KV-event publisher slot: one per
independent KV cache (pp/attn-TP/attn-CP rank 0). Shared by independent KV cache (pp/attn-TP/attn-CP rank 0). Shared by
`SchedulerKvEventsPublisher` and `SchedulerLoadPublisher`, which must `SchedulerKvEventsPublisher` and `SchedulerLoadPublisher`, which must
gate identically or their /server_info-derived ports disagree. gate identically or their /server_info-derived ports disagree.
""" """
parallel = get_parallel()
return bool( return bool(
kv_events_config kv_events_config
and ps.pp_rank == 0 and parallel.pp_rank == 0
and ps.attn_tp_rank == 0 and parallel.attn_tp_rank == 0
and ps.attn_cp_rank == 0 and parallel.attn_cp_rank == 0
) )
@@ -3045,17 +3045,37 @@ def patch_pipeline_parallel_group(pp_group: GroupCoordinator):
@contextmanager @contextmanager
def patch_tensor_parallel_group(tp_group: GroupCoordinator): def patch_tensor_parallel_group(tp_group: GroupCoordinator, *, owns_attention: bool):
"""Run under a different tensor-parallel group until this scope ends. """Run under a different tensor-parallel group until this scope ends.
This is for draft workers of speculative decoding, which run the draft model This is for draft workers of speculative decoding, which run the draft model
at the target's attention-TP width rather than its global TP width. at the target's attention-TP width rather than its global TP width.
The scope replaces both the module global that ``get_tp_group()`` reads and The scope replaces both the module global that ``get_tp_group()`` reads and
the three members the runtime context answers with. the members the runtime context answers with.
Which members depends on what the draft is, and only the worker knows: the
same call site hands over an attention-TP slice for one draft and the
target's whole TP group for another, so this cannot be read off the group.
``owns_attention`` says which. A draft that owns its attention topology
runs the whole model on the group being installed -- there is no
attention-DP replica inside it, so its attention identity is the group
itself, one replica, one context shard, and no expert dimension either.
Leaving those names on the target's answers is what lets a draft read
report a replica count the draft does not have.
A draft that does not own it was built outside any scope and keeps the
target's layout: the process is still one of several attention-DP replicas
and still gathers with them. Claiming one replica there is the same error
in the other direction, and the reader that acts on it is a collective -- a
DP gather takes its buffer size from the replica count and its communicator
from this group, so the two stop agreeing.
Args: Args:
tp_group (GroupCoordinator): the tp group coordinator tp_group (GroupCoordinator): the tp group coordinator
owns_attention (bool): whether the draft's attention topology is this
group, decided by the worker where it builds its draft runner
""" """
global _TP_STATE_PATCHED global _TP_STATE_PATCHED
@@ -3065,12 +3085,22 @@ def patch_tensor_parallel_group(tp_group: GroupCoordinator):
old_tp_group = get_tp_group() old_tp_group = get_tp_group()
global _TP global _TP
_TP = tp_group _TP = tp_group
narrowed = dict(
tp_size=tp_group.world_size,
tp_rank=tp_group.rank_in_group,
tp_group=tp_group,
)
if owns_attention:
narrowed.update(
attn_tp_size=tp_group.world_size,
attn_tp_rank=tp_group.rank_in_group,
attn_dp_size=1,
attn_dp_rank=0,
attn_cp_size=1,
attn_cp_rank=0,
)
try: try:
with get_parallel().override( with get_parallel().override(**narrowed):
tp_size=tp_group.world_size,
tp_rank=tp_group.rank_in_group,
tp_group=tp_group,
):
yield yield
finally: finally:
_TP_STATE_PATCHED = False _TP_STATE_PATCHED = False
+3 -7
View File
@@ -801,7 +801,6 @@ class Scheduler(
) -> None: ) -> None:
self.metrics_collector_context = SchedulerMetricsCollector.init_new( self.metrics_collector_context = SchedulerMetricsCollector.init_new(
server_args=self.server_args, server_args=self.server_args,
ps=self.ps,
tp_rank=tp_rank, tp_rank=tp_rank,
pp_rank=pp_rank, pp_rank=pp_rank,
dp_rank=dp_rank, dp_rank=dp_rank,
@@ -843,7 +842,7 @@ class Scheduler(
try: try:
self.load_snapshot_writer = create_load_snapshot_writer( self.load_snapshot_writer = create_load_snapshot_writer(
port_args, port_args,
self.ps.dp_size, get_parallel().dp_size,
dp_rank, dp_rank,
publish_interval=get_observability().load_snapshot_publish_interval, publish_interval=get_observability().load_snapshot_publish_interval,
) )
@@ -1432,7 +1431,7 @@ class Scheduler(
) )
else: else:
self.prefill_delayer = PrefillDelayer( self.prefill_delayer = PrefillDelayer(
dp_size=self.ps.dp_size, dp_size=get_parallel().dp_size,
attn_tp_size=get_parallel().attn_tp_size, attn_tp_size=get_parallel().attn_tp_size,
cpu_group=self.tp_cpu_group, cpu_group=self.tp_cpu_group,
device_group=self.tp_group.device_group, device_group=self.tp_group.device_group,
@@ -2340,7 +2339,6 @@ class Scheduler(
recv_skipper=self.recv_skipper, recv_skipper=self.recv_skipper,
input_blocker=self.input_blocker, input_blocker=self.input_blocker,
mm_receiver=self.mm_receiver, mm_receiver=self.mm_receiver,
ps=self.ps,
tp_group=self.tp_group, tp_group=self.tp_group,
tp_cpu_group=self.tp_cpu_group, tp_cpu_group=self.tp_cpu_group,
attn_tp_group=self.attn_tp_group, attn_tp_group=self.attn_tp_group,
@@ -2429,10 +2427,9 @@ class Scheduler(
def init_kv_events_publisher(self) -> None: def init_kv_events_publisher(self) -> None:
self.kv_events_publisher = SchedulerKvEventsPublisher( self.kv_events_publisher = SchedulerKvEventsPublisher(
kv_events_config=get_observability().kv_events_config, kv_events_config=get_observability().kv_events_config,
ps=self.ps,
attn_tp_rank=get_parallel().attn_tp_rank, attn_tp_rank=get_parallel().attn_tp_rank,
attn_cp_rank=get_parallel().attn_cp_rank, attn_cp_rank=get_parallel().attn_cp_rank,
attn_dp_rank=self.ps.attn_dp_rank, attn_dp_rank=get_parallel().attn_dp_rank,
dp_rank=get_parallel().dp_rank, dp_rank=get_parallel().dp_rank,
tree_cache=self.tree_cache, tree_cache=self.tree_cache,
send_metrics_from_scheduler=self.ipc_channels.send_metrics_from_scheduler, send_metrics_from_scheduler=self.ipc_channels.send_metrics_from_scheduler,
@@ -2448,7 +2445,6 @@ class Scheduler(
# instead of walking the queues itself. # instead of walking the queues itself.
self.load_publisher = SchedulerLoadPublisher( self.load_publisher = SchedulerLoadPublisher(
kv_events_config=get_observability().kv_events_config, kv_events_config=get_observability().kv_events_config,
ps=self.ps,
load_publish_endpoint=get_observability().load_publish_endpoint, load_publish_endpoint=get_observability().load_publish_endpoint,
publish_interval=get_observability().load_snapshot_publish_interval, publish_interval=get_observability().load_snapshot_publish_interval,
) )
@@ -22,7 +22,6 @@ from sglang.srt.managers.io_struct import hook_custom_types, sock_send
from sglang.srt.runtime_context import get_parallel from sglang.srt.runtime_context import get_parallel
if TYPE_CHECKING: if TYPE_CHECKING:
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
@@ -46,7 +45,6 @@ hook_custom_types(KvMetrics)
@dataclass(kw_only=True, slots=True) @dataclass(kw_only=True, slots=True)
class SchedulerKvEventsPublisher: class SchedulerKvEventsPublisher:
kv_events_config: Optional[str] kv_events_config: Optional[str]
ps: ParallelState
attn_tp_rank: int attn_tp_rank: int
attn_cp_rank: int attn_cp_rank: int
attn_dp_rank: int attn_dp_rank: int
@@ -63,13 +61,14 @@ class SchedulerKvEventsPublisher:
self.init_kv_events(self.kv_events_config) self.init_kv_events(self.kv_events_config)
def init_kv_events(self, kv_events_config: Optional[str]): def init_kv_events(self, kv_events_config: Optional[str]):
self.enable_kv_cache_events = is_kv_publisher_rank(kv_events_config, self.ps) self.enable_kv_cache_events = is_kv_publisher_rank(kv_events_config)
if self.enable_kv_cache_events: if self.enable_kv_cache_events:
parallel = get_parallel()
self.kv_event_publisher = EventPublisherFactory.create( self.kv_event_publisher = EventPublisherFactory.create(
kv_events_config, kv_events_config,
select_kv_publisher_dp_rank( select_kv_publisher_dp_rank(
self.ps.attn_dp_size, self.ps.attn_dp_rank, get_parallel().dp_rank parallel.attn_dp_size, parallel.attn_dp_rank, parallel.dp_rank
), ),
) )
@@ -42,10 +42,10 @@ from sglang.srt.disaggregation.kv_events import (
resolve_load_pub_range, resolve_load_pub_range,
select_kv_publisher_dp_rank, select_kv_publisher_dp_rank,
) )
from sglang.srt.runtime_context import get_parallel
from sglang.srt.utils.network import NetworkAddress, is_zmq_endpoint_ipv6 from sglang.srt.utils.network import NetworkAddress, is_zmq_endpoint_ipv6
if TYPE_CHECKING: if TYPE_CHECKING:
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
from sglang.srt.managers.load_snapshot import LoadSnapshot from sglang.srt.managers.load_snapshot import LoadSnapshot
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -132,7 +132,6 @@ class SchedulerLoadPublisher:
self, self,
*, *,
kv_events_config: Optional[str], kv_events_config: Optional[str],
ps: ParallelState,
load_publish_endpoint: Optional[str] = None, load_publish_endpoint: Optional[str] = None,
publish_interval: int = LOAD_PUBLISH_INTERVAL, publish_interval: int = LOAD_PUBLISH_INTERVAL,
) -> None: ) -> None:
@@ -147,7 +146,7 @@ class SchedulerLoadPublisher:
self._last_counts: Optional[tuple] = None self._last_counts: Optional[tuple] = None
self._last_publish_ts = 0.0 self._last_publish_ts = 0.0
self._publish_failed = False self._publish_failed = False
if not is_kv_publisher_rank(kv_events_config, ps): if not is_kv_publisher_rank(kv_events_config):
return return
try: try:
cfg = KVEventsConfig.from_cli(kv_events_config) cfg = KVEventsConfig.from_cli(kv_events_config)
@@ -165,7 +164,7 @@ class SchedulerLoadPublisher:
resolved, reason = resolve_load_pub_range( resolved, reason = resolve_load_pub_range(
kv_endpoint=cfg.endpoint, kv_endpoint=cfg.endpoint,
replay_endpoint=cfg.replay_endpoint, replay_endpoint=cfg.replay_endpoint,
dp_size=ps.dp_size, dp_size=get_parallel().dp_size,
load_publish_endpoint=load_publish_endpoint, load_publish_endpoint=load_publish_endpoint,
) )
if resolved is None: if resolved is None:
@@ -173,8 +172,9 @@ class SchedulerLoadPublisher:
logger.warning("load-publisher disabled: %s", reason) logger.warning("load-publisher disabled: %s", reason)
return return
host, base = resolved host, base = resolved
parallel = get_parallel()
self._rank = select_kv_publisher_dp_rank( self._rank = select_kv_publisher_dp_rank(
ps.attn_dp_size, ps.attn_dp_rank, ps.dp_rank parallel.attn_dp_size, parallel.attn_dp_rank, parallel.dp_rank
) )
endpoint = NetworkAddress(host, base + self._rank).to_tcp() endpoint = NetworkAddress(host, base + self._rank).to_tcp()
try: try:
@@ -287,7 +287,7 @@ class SchedulerProfilerManager:
if get_parallel().tp_rank != 0: if get_parallel().tp_rank != 0:
return "" return ""
if self.ps.dp_size > 1 and get_parallel().dp_rank != 0: if get_parallel().dp_size > 1 and get_parallel().dp_rank != 0:
return "" return ""
if get_parallel().pp_size > 1 and get_parallel().pp_rank != 0: if get_parallel().pp_size > 1 and get_parallel().pp_rank != 0:
return "" return ""
@@ -342,7 +342,7 @@ class SchedulerProfilerManager:
filename_parts = [self.profile_id, f"TP-{get_parallel().tp_rank}"] filename_parts = [self.profile_id, f"TP-{get_parallel().tp_rank}"]
# Only add other ranks if parallelism is enabled (size > 1) # Only add other ranks if parallelism is enabled (size > 1)
if self.ps.dp_size > 1: if get_parallel().dp_size > 1:
filename_parts.append(f"DP-{get_parallel().dp_rank}") filename_parts.append(f"DP-{get_parallel().dp_rank}")
if get_parallel().pp_size > 1: if get_parallel().pp_size > 1:
filename_parts.append(f"PP-{get_parallel().pp_rank}") filename_parts.append(f"PP-{get_parallel().pp_rank}")
@@ -46,7 +46,6 @@ from sglang.srt.utils import (
if TYPE_CHECKING: if TYPE_CHECKING:
from sglang.srt.configs.model_config import ModelConfig from sglang.srt.configs.model_config import ModelConfig
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
from sglang.srt.rust_server.server import RustServer from sglang.srt.rust_server.server import RustServer
from sglang.srt.server_args import ServerArgs from sglang.srt.server_args import ServerArgs
from sglang.test.scripted_runtime.scheduler_hook import ScriptedSchedulerHook from sglang.test.scripted_runtime.scheduler_hook import ScriptedSchedulerHook
@@ -64,7 +63,6 @@ class SchedulerRequestReceiver:
recv_skipper: Any recv_skipper: Any
input_blocker: Any input_blocker: Any
mm_receiver: Any mm_receiver: Any
ps: ParallelState
tp_group: Any tp_group: Any
tp_cpu_group: Any tp_cpu_group: Any
attn_tp_group: Any attn_tp_group: Any
@@ -120,7 +118,7 @@ class SchedulerRequestReceiver:
def _pull_raw_reqs(self) -> Optional[List]: def _pull_raw_reqs(self) -> Optional[List]:
if get_parallel().pp_rank == 0: if get_parallel().pp_rank == 0:
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:
recv_reqs = [] recv_reqs = []
# Rust ringbuffer backend: drain the in-process ring fed by the # Rust ringbuffer backend: drain the in-process ring fed by the
@@ -152,16 +150,18 @@ class SchedulerRequestReceiver:
else: else:
recv_reqs = None recv_reqs = None
else: else:
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 = ( dp_offset = (
self.ps.attn_dp_rank * self.ps.attn_cp_size * self.ps.attn_tp_size get_parallel().attn_dp_rank
* get_parallel().attn_cp_size
* get_parallel().attn_tp_size
) )
recv_reqs = point_to_point_pyobj( recv_reqs = point_to_point_pyobj(
[], [],
get_parallel().pp_rank * self.ps.tp_size + dp_offset, get_parallel().pp_rank * get_parallel().tp_size + dp_offset,
self.world_group.cpu_group, self.world_group.cpu_group,
(get_parallel().pp_rank - 1) * self.ps.tp_size + dp_offset, (get_parallel().pp_rank - 1) * get_parallel().tp_size + dp_offset,
get_parallel().pp_rank * self.ps.tp_size + dp_offset, get_parallel().pp_rank * get_parallel().tp_size + dp_offset,
) )
else: else:
recv_reqs = None recv_reqs = None
@@ -176,7 +176,7 @@ class SchedulerRequestReceiver:
""" """
local_reqs = local_reqs or [] local_reqs = local_reqs or []
if get_parallel().enable_dp_attention: if get_parallel().enable_dp_attention:
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:
work_reqs, control_reqs = self._split_work_and_control_reqs(recv_reqs) work_reqs, control_reqs = self._split_work_and_control_reqs(recv_reqs)
work_reqs.extend(local_reqs) work_reqs.extend(local_reqs)
else: else:
@@ -196,7 +196,7 @@ class SchedulerRequestReceiver:
) )
if _local_ctrl: if _local_ctrl:
control_reqs = attn_cp_tp_broadcast_pyobj(control_reqs) control_reqs = attn_cp_tp_broadcast_pyobj(control_reqs)
elif self.ps.tp_size != 1: elif get_parallel().tp_size != 1:
control_reqs = broadcast_pyobj( control_reqs = broadcast_pyobj(
control_reqs, control_reqs,
self.tp_group.rank, self.tp_group.rank,
@@ -207,7 +207,7 @@ class SchedulerRequestReceiver:
else: else:
if recv_reqs is not None: if recv_reqs is not None:
recv_reqs = [*recv_reqs, *local_reqs] recv_reqs = [*recv_reqs, *local_reqs]
if self.ps.tp_size != 1: if get_parallel().tp_size != 1:
recv_reqs = broadcast_pyobj( recv_reqs = broadcast_pyobj(
recv_reqs, recv_reqs,
self.tp_group.rank, self.tp_group.rank,
@@ -269,11 +269,11 @@ class SchedulerRequestReceiver:
# 1. wait until every rank has opened the shared feature segments # 1. wait until every rank has opened the shared feature segments
parallel = get_parallel() parallel = get_parallel()
if parallel.enable_dp_attention: if parallel.enable_dp_attention:
if self.ps.attn_tp_size > 1: if parallel.attn_tp_size > 1:
barrier(group=self.attn_tp_cpu_group) barrier(group=self.attn_tp_cpu_group)
if self.ps.attn_cp_size > 1: if parallel.attn_cp_size > 1:
barrier(group=self.attn_cp_cpu_group) barrier(group=self.attn_cp_cpu_group)
elif self.ps.tp_size > 1: elif parallel.tp_size > 1:
barrier(group=self.tp_cpu_group) barrier(group=self.tp_cpu_group)
# 2. materialize independently so one bad VLM request does not stop the loop # 2. materialize independently so one bad VLM request does not stop the loop
@@ -293,11 +293,11 @@ class SchedulerRequestReceiver:
# 3. all ranks reject the same requests before entering model collectives # 3. all ranks reject the same requests before entering model collectives
if parallel.enable_dp_attention: if parallel.enable_dp_attention:
if self.ps.attn_tp_size > 1: if parallel.attn_tp_size > 1:
all_reduce(failed, op=ReduceOp.MAX, group=self.attn_tp_cpu_group) all_reduce(failed, op=ReduceOp.MAX, group=self.attn_tp_cpu_group)
if self.ps.attn_cp_size > 1: if parallel.attn_cp_size > 1:
all_reduce(failed, op=ReduceOp.MAX, group=self.attn_cp_cpu_group) all_reduce(failed, op=ReduceOp.MAX, group=self.attn_cp_cpu_group)
elif self.ps.tp_size > 1: elif parallel.tp_size > 1:
all_reduce(failed, op=ReduceOp.MAX, group=self.tp_cpu_group) all_reduce(failed, op=ReduceOp.MAX, group=self.tp_cpu_group)
error = MMInputsProcessError( error = MMInputsProcessError(
@@ -815,7 +815,7 @@ class SchedulerPPMixin:
p2p_work = [] p2p_work = []
if get_parallel().attn_tp_rank == 0 and get_parallel().attn_cp_rank == 0: if get_parallel().attn_tp_rank == 0 and get_parallel().attn_cp_rank == 0:
dp_offset = ( dp_offset = (
self.ps.attn_dp_rank get_parallel().attn_dp_rank
* get_parallel().attn_cp_size * get_parallel().attn_cp_size
* get_parallel().attn_tp_size * get_parallel().attn_tp_size
) )
@@ -834,7 +834,7 @@ class SchedulerPPMixin:
def _pp_recv_pyobj_from_prev_stage(self: Scheduler): def _pp_recv_pyobj_from_prev_stage(self: Scheduler):
if get_parallel().attn_tp_rank == 0 and get_parallel().attn_cp_rank == 0: if get_parallel().attn_tp_rank == 0 and get_parallel().attn_cp_rank == 0:
dp_offset = ( dp_offset = (
self.ps.attn_dp_rank get_parallel().attn_dp_rank
* get_parallel().attn_cp_size * get_parallel().attn_cp_size
* get_parallel().attn_tp_size * get_parallel().attn_tp_size
) )
@@ -1103,7 +1103,6 @@ class SchedulerMetricsCollector(_StatLoggerDIMixin):
cls, cls,
*, *,
server_args: ServerArgs, server_args: ServerArgs,
ps: Any,
tp_rank: int, tp_rank: int,
pp_rank: int, pp_rank: int,
dp_rank: Optional[int], dp_rank: Optional[int],
@@ -1112,7 +1111,8 @@ class SchedulerMetricsCollector(_StatLoggerDIMixin):
enable_hierarchical_cache: bool, enable_hierarchical_cache: bool,
) -> SchedulerMetricsCollectorContext: ) -> SchedulerMetricsCollectorContext:
enable_metrics = get_observability().enable_metrics enable_metrics = get_observability().enable_metrics
is_stats_logging_rank = ps.attn_tp_rank == 0 parallel = get_parallel()
is_stats_logging_rank = parallel.attn_tp_rank == 0
current_scheduler_metrics_enabled = enable_metrics and ( current_scheduler_metrics_enabled = enable_metrics and (
is_stats_logging_rank is_stats_logging_rank
or get_observability().enable_metrics_for_all_schedulers or get_observability().enable_metrics_for_all_schedulers
@@ -1120,8 +1120,8 @@ class SchedulerMetricsCollector(_StatLoggerDIMixin):
enable_kv_cache_events = bool( enable_kv_cache_events = bool(
get_observability().kv_events_config get_observability().kv_events_config
and get_parallel().pp_rank == 0 and get_parallel().pp_rank == 0
and ps.attn_tp_rank == 0 and parallel.attn_tp_rank == 0
and ps.attn_cp_rank == 0 and parallel.attn_cp_rank == 0
) )
collector: Optional[SchedulerMetricsCollector] = None collector: Optional[SchedulerMetricsCollector] = None
if enable_metrics: if enable_metrics:
@@ -1136,7 +1136,7 @@ class SchedulerMetricsCollector(_StatLoggerDIMixin):
"engine_type": engine_type, "engine_type": engine_type,
"tp_rank": tp_rank, "tp_rank": tp_rank,
"pp_rank": pp_rank, "pp_rank": pp_rank,
"moe_ep_rank": ps.moe_ep_rank, "moe_ep_rank": parallel.moe_ep_rank,
} }
if enable_priority_scheduling: if enable_priority_scheduling:
labels["priority"] = "" labels["priority"] = ""
@@ -400,8 +400,16 @@ class DFlashWorkerV2(BaseSpecWorker):
self.draft_tp_context = ( self.draft_tp_context = (
draft_tp_context if get_parallel().enable_dp_attention else empty_context draft_tp_context if get_parallel().enable_dp_attention else empty_context
) )
if get_parallel().enable_dp_attention: # One decision, used twice: whether the draft runs on an attention-TP
draft_init_ctx = draft_tp_context(get_parallel().attn_tp_group) # slice of its own. It picks how the runner is built, and then what the
# scope may say about attention every time it is entered -- a draft
# built outside the scope keeps the target's replica count and still
# gathers with it.
self.draft_owns_attention = get_parallel().enable_dp_attention
if self.draft_owns_attention:
draft_init_ctx = draft_tp_context(
get_parallel().attn_tp_group, owns_attention=True
)
else: else:
draft_init_ctx = empty_context() draft_init_ctx = empty_context()
with draft_pp_context(), draft_init_ctx: with draft_pp_context(), draft_init_ctx:
@@ -602,7 +610,10 @@ class DFlashWorkerV2(BaseSpecWorker):
def init_attention_backends(self): def init_attention_backends(self):
with ( with (
draft_pp_context(), draft_pp_context(),
self.draft_tp_context(self.draft_model_runner.tp_group), self.draft_tp_context(
self.draft_model_runner.tp_group,
owns_attention=self.draft_owns_attention,
),
): ):
self._draft_worker.init_attention_backends() self._draft_worker.init_attention_backends()
self._need_mamba_verify_commit = mambaish_config( self._need_mamba_verify_commit = mambaish_config(
@@ -615,7 +626,10 @@ class DFlashWorkerV2(BaseSpecWorker):
def init_cuda_graphs(self): def init_cuda_graphs(self):
with ( with (
draft_pp_context(), draft_pp_context(),
self.draft_tp_context(self.draft_model_runner.tp_group), self.draft_tp_context(
self.draft_model_runner.tp_group,
owns_attention=self.draft_owns_attention,
),
): ):
capture_decode_cuda_graph = ( capture_decode_cuda_graph = (
get_exec().graph.cuda_graph_config.decode.backend != Backend.DISABLED get_exec().graph.cuda_graph_config.decode.backend != Backend.DISABLED
@@ -1818,7 +1832,10 @@ class DFlashWorkerV2(BaseSpecWorker):
with ( with (
torch.inference_mode(), torch.inference_mode(),
self.draft_tp_context(self.draft_model_runner.tp_group), self.draft_tp_context(
self.draft_model_runner.tp_group,
owns_attention=self.draft_owns_attention,
),
): ):
ctx_hidden = self.draft_model.project_target_hidden(target_hidden) ctx_hidden = self.draft_model.project_target_hidden(target_hidden)
@@ -2513,7 +2530,10 @@ class DFlashWorkerV2(BaseSpecWorker):
with ( with (
torch.inference_mode(), torch.inference_mode(),
self.draft_tp_context(self.draft_model_runner.tp_group), self.draft_tp_context(
self.draft_model_runner.tp_group,
owns_attention=self.draft_owns_attention,
),
): ):
draft_out = self.draft_model_runner.forward(forward_batch) draft_out = self.draft_model_runner.forward(forward_batch)
draft_logits_output = draft_out.logits_output draft_logits_output = draft_out.logits_output
@@ -2572,7 +2592,10 @@ class DFlashWorkerV2(BaseSpecWorker):
self._draft_sampler.q_out[:bs], self._draft_sampler.q_out[:bs],
) )
elif self.selector is not None: elif self.selector is not None:
with self.draft_tp_context(self.draft_model_runner.tp_group): with self.draft_tp_context(
self.draft_model_runner.tp_group,
owns_attention=self.draft_owns_attention,
):
draft_next = self._propose_selector_block( draft_next = self._propose_selector_block(
draft_logits_output=draft_logits_output, draft_logits_output=draft_logits_output,
bs=bs, bs=bs,
@@ -2585,7 +2608,10 @@ class DFlashWorkerV2(BaseSpecWorker):
if draft_hidden is None: if draft_hidden is None:
raise RuntimeError("DFLASH draft model returned no hidden states.") raise RuntimeError("DFLASH draft model returned no hidden states.")
draft_hidden = draft_hidden.view(bs, int(self.block_size), -1) draft_hidden = draft_hidden.view(bs, int(self.block_size), -1)
with self.draft_tp_context(self.draft_model_runner.tp_group): with self.draft_tp_context(
self.draft_model_runner.tp_group,
owns_attention=self.draft_owns_attention,
):
draft_next = self._greedy_sample_from_vocab_parallel_head( draft_next = self._greedy_sample_from_vocab_parallel_head(
hidden_states=draft_hidden[:, 1:, :].reshape( hidden_states=draft_hidden[:, 1:, :].reshape(
-1, draft_hidden.shape[-1] -1, draft_hidden.shape[-1]
@@ -236,7 +236,7 @@ class DraftBlockProposer:
def _base_logits_context(self): def _base_logits_context(self):
if self._dp_moe_sync: if self._dp_moe_sync:
return draft_tp_context(get_parallel().attn_tp_group) return draft_tp_context(get_parallel().attn_tp_group, owns_attention=True)
return nullcontext() return nullcontext()
def propose( def propose(
@@ -424,7 +424,7 @@ class DSparkWorkerV2(BaseSpecWorker):
def _draft_context(self): def _draft_context(self):
if self._draft_dp_context_enabled: if self._draft_dp_context_enabled:
return draft_tp_context(get_parallel().attn_tp_group) return draft_tp_context(get_parallel().attn_tp_group, owns_attention=True)
return nullcontext() return nullcontext()
def alloc_memory_pool( def alloc_memory_pool(
@@ -265,11 +265,17 @@ class EagleDraftWorker(EagleDraftWorkerBase):
self._rebuild_topk1_chain_buffers() self._rebuild_topk1_chain_buffers()
# Load draft model weights only. # Load draft model weights only.
if ( # One decision, used twice: whether the draft runs on an attention-TP
# slice of its own. It picks how the runner is built, and then what the
# scope may say about attention every time it is entered -- a draft
# built outside the scope keeps the target's replica count and still
# gathers with it.
self.draft_owns_attention = (
get_parallel().enable_dp_attention get_parallel().enable_dp_attention
and self.speculative_algorithm.is_eagle3() and self.speculative_algorithm.is_eagle3()
): )
ctx = draft_tp_context(get_parallel().attn_tp_group) if self.draft_owns_attention:
ctx = draft_tp_context(get_parallel().attn_tp_group, owns_attention=True)
else: else:
ctx = empty_context() ctx = empty_context()
with ( with (
@@ -339,7 +345,10 @@ class EagleDraftWorker(EagleDraftWorkerBase):
def init_attention_backends(self): def init_attention_backends(self):
with ( with (
draft_pp_context(), draft_pp_context(),
self.draft_tp_context(self.draft_runner.tp_group), self.draft_tp_context(
self.draft_runner.tp_group,
owns_attention=self.draft_owns_attention,
),
speculative_moe_backend_context(), speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(), speculative_moe_a2a_backend_context(),
): ):
@@ -349,7 +358,10 @@ class EagleDraftWorker(EagleDraftWorkerBase):
def init_cuda_graphs(self): def init_cuda_graphs(self):
with ( with (
draft_pp_context(), draft_pp_context(),
self.draft_tp_context(self.draft_runner.tp_group), self.draft_tp_context(
self.draft_runner.tp_group,
owns_attention=self.draft_owns_attention,
),
speculative_moe_backend_context(), speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(), speculative_moe_a2a_backend_context(),
): ):
@@ -1358,7 +1370,8 @@ class EAGLEWorkerV2(BaseSpecWorker):
if self.adaptive_controller is not None: if self.adaptive_controller is not None:
with ( with (
self._draft_worker.draft_tp_context( self._draft_worker.draft_tp_context(
self._draft_worker.draft_runner.tp_group self._draft_worker.draft_runner.tp_group,
owns_attention=self._draft_worker.draft_owns_attention,
), ),
speculative_moe_backend_context(), speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(), speculative_moe_a2a_backend_context(),
@@ -1418,7 +1431,8 @@ class EAGLEWorkerV2(BaseSpecWorker):
# Draft prefill # Draft prefill
with ( with (
self.draft_worker.draft_tp_context( self.draft_worker.draft_tp_context(
self.draft_worker.draft_runner.tp_group self.draft_worker.draft_runner.tp_group,
owns_attention=self.draft_worker.draft_owns_attention,
), ),
speculative_moe_backend_context(), speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(), speculative_moe_a2a_backend_context(),
@@ -1465,7 +1479,8 @@ class EAGLEWorkerV2(BaseSpecWorker):
else: else:
with ( with (
self.draft_worker.draft_tp_context( self.draft_worker.draft_tp_context(
self.draft_worker.draft_runner.tp_group self.draft_worker.draft_runner.tp_group,
owns_attention=self.draft_worker.draft_owns_attention,
), ),
speculative_moe_backend_context(), speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(), speculative_moe_a2a_backend_context(),
@@ -1490,7 +1505,8 @@ class EAGLEWorkerV2(BaseSpecWorker):
else: else:
with ( with (
self.draft_worker.draft_tp_context( self.draft_worker.draft_tp_context(
self.draft_worker.draft_runner.tp_group self.draft_worker.draft_runner.tp_group,
owns_attention=self.draft_worker.draft_owns_attention,
), ),
speculative_moe_backend_context(), speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(), speculative_moe_a2a_backend_context(),
@@ -1522,7 +1538,8 @@ class EAGLEWorkerV2(BaseSpecWorker):
batch.seq_lens_sum = int(batch.seq_lens_cpu.sum()) batch.seq_lens_sum = int(batch.seq_lens_cpu.sum())
with ( with (
self.draft_worker.draft_tp_context( self.draft_worker.draft_tp_context(
self.draft_worker.draft_runner.tp_group self.draft_worker.draft_runner.tp_group,
owns_attention=self.draft_worker.draft_owns_attention,
), ),
speculative_moe_backend_context(), speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(), speculative_moe_a2a_backend_context(),
@@ -166,6 +166,10 @@ class FrozenKVMTPDraftWorker(EagleDraftWorkerBase, TpModelWorker):
self.kv_context: Optional[FrozenKVMTPContext] = None self.kv_context: Optional[FrozenKVMTPContext] = None
# Built above under the pipeline scope only, so this runner carries the
# target's attention topology: entering the tensor scope later swaps the
# communicator without giving the draft a replica of its own.
self.draft_owns_attention = False
self.draft_tp_context = ( self.draft_tp_context = (
draft_tp_context if get_parallel().enable_dp_attention else empty_context draft_tp_context if get_parallel().enable_dp_attention else empty_context
) )
@@ -206,7 +210,10 @@ class FrozenKVMTPDraftWorker(EagleDraftWorkerBase, TpModelWorker):
def init_attention_backends(self): def init_attention_backends(self):
with ( with (
draft_pp_context(), draft_pp_context(),
self.draft_tp_context(self.draft_model_runner.tp_group), self.draft_tp_context(
self.draft_model_runner.tp_group,
owns_attention=self.draft_owns_attention,
),
speculative_moe_backend_context(), speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(), speculative_moe_a2a_backend_context(),
): ):
@@ -217,7 +224,10 @@ class FrozenKVMTPDraftWorker(EagleDraftWorkerBase, TpModelWorker):
def init_cuda_graphs(self): def init_cuda_graphs(self):
with ( with (
draft_pp_context(), draft_pp_context(),
self.draft_tp_context(self.draft_model_runner.tp_group), self.draft_tp_context(
self.draft_model_runner.tp_group,
owns_attention=self.draft_owns_attention,
),
speculative_moe_backend_context(), speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(), speculative_moe_a2a_backend_context(),
): ):
@@ -768,7 +778,8 @@ class FrozenKVMTPWorkerV2(EAGLEWorkerV2):
# Draft prefill seed (no forward). # Draft prefill seed (no forward).
with ( with (
self.draft_worker.draft_tp_context( self.draft_worker.draft_tp_context(
self.draft_worker.draft_runner.tp_group self.draft_worker.draft_runner.tp_group,
owns_attention=self.draft_worker.draft_owns_attention,
), ),
speculative_moe_backend_context(), speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(), speculative_moe_a2a_backend_context(),
@@ -790,7 +801,8 @@ class FrozenKVMTPWorkerV2(EAGLEWorkerV2):
batch.spec_info = self.draft_worker._idle_seed() batch.spec_info = self.draft_worker._idle_seed()
with ( with (
self.draft_worker.draft_tp_context( self.draft_worker.draft_tp_context(
self.draft_worker.draft_runner.tp_group self.draft_worker.draft_runner.tp_group,
owns_attention=self.draft_worker.draft_owns_attention,
), ),
speculative_moe_backend_context(), speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(), speculative_moe_a2a_backend_context(),
@@ -805,7 +817,8 @@ class FrozenKVMTPWorkerV2(EAGLEWorkerV2):
on_publish(batch_output.new_seq_lens) on_publish(batch_output.new_seq_lens)
with ( with (
self.draft_worker.draft_tp_context( self.draft_worker.draft_tp_context(
self.draft_worker.draft_runner.tp_group self.draft_worker.draft_runner.tp_group,
owns_attention=self.draft_worker.draft_owns_attention,
), ),
speculative_moe_backend_context(), speculative_moe_backend_context(),
speculative_moe_a2a_backend_context(), speculative_moe_a2a_backend_context(),
@@ -193,6 +193,11 @@ class MultiLayerEagleDraftWorker(EagleDraftWorkerBase):
"InklingForConditionalGenerationMTP", "InklingForConditionalGenerationMTP",
"GigaChat35ForCausalLMNextN", "GigaChat35ForCausalLMNextN",
] ]
# The draft runner is built outside any tensor-parallel scope, so it
# carries the target's topology: entering the scope later swaps the
# communicator without making this process a draft with an attention
# replica of its own. It still gathers with the target's replicas.
self.draft_owns_attention = False
self.draft_tp_context = ( self.draft_tp_context = (
draft_tp_context if get_parallel().enable_dp_attention else empty_context draft_tp_context if get_parallel().enable_dp_attention else empty_context
) )
@@ -224,7 +229,10 @@ class MultiLayerEagleDraftWorker(EagleDraftWorkerBase):
def init_attention_backends(self): def init_attention_backends(self):
with ( with (
draft_pp_context(), draft_pp_context(),
self.draft_tp_context(self.draft_runner_list[0].tp_group), self.draft_tp_context(
self.draft_runner_list[0].tp_group,
owns_attention=self.draft_owns_attention,
),
speculative_moe_backend_context(), speculative_moe_backend_context(),
): ):
super().init_attention_backends() super().init_attention_backends()
@@ -232,7 +240,10 @@ class MultiLayerEagleDraftWorker(EagleDraftWorkerBase):
def init_cuda_graphs(self): def init_cuda_graphs(self):
with ( with (
draft_pp_context(), draft_pp_context(),
self.draft_tp_context(self.draft_runner_list[0].tp_group), self.draft_tp_context(
self.draft_runner_list[0].tp_group,
owns_attention=self.draft_owns_attention,
),
speculative_moe_backend_context(), speculative_moe_backend_context(),
): ):
super().init_cuda_graphs() super().init_cuda_graphs()
+2 -2
View File
@@ -711,10 +711,10 @@ def draft_pp_context():
@contextmanager @contextmanager
def draft_tp_context(tp_group: GroupCoordinator): def draft_tp_context(tp_group: GroupCoordinator, *, owns_attention: bool):
# Draft model doesn't use dp and has its own tp group. # Draft model doesn't use dp and has its own tp group.
# We disable mscclpp now because it doesn't support 2 comm groups. # We disable mscclpp now because it doesn't support 2 comm groups.
with patch_tensor_parallel_group(tp_group): with patch_tensor_parallel_group(tp_group, owns_attention=owns_attention):
yield yield
@@ -94,6 +94,11 @@ class StandaloneDraftWorker(EagleDraftWorker):
# Alias for better readability # Alias for better readability
self.draft_runner = self.draft_worker.model_runner self.draft_runner = self.draft_worker.model_runner
# The draft runner is built outside any tensor-parallel scope, so it
# carries the target's topology: entering the scope later swaps the
# communicator without making this process a draft with an attention
# replica of its own. It still gathers with the target's replicas.
self.draft_owns_attention = False
self.draft_tp_context = ( self.draft_tp_context = (
draft_tp_context if get_parallel().enable_dp_attention else empty_context draft_tp_context if get_parallel().enable_dp_attention else empty_context
) )
@@ -132,14 +137,20 @@ class StandaloneDraftWorker(EagleDraftWorker):
def init_attention_backends(self): def init_attention_backends(self):
with ( with (
self.draft_tp_context(self.draft_runner.tp_group), self.draft_tp_context(
self.draft_runner.tp_group,
owns_attention=self.draft_owns_attention,
),
speculative_moe_backend_context(), speculative_moe_backend_context(),
): ):
super().init_attention_backends() super().init_attention_backends()
def init_cuda_graphs(self): def init_cuda_graphs(self):
with ( with (
self.draft_tp_context(self.draft_runner.tp_group), self.draft_tp_context(
self.draft_runner.tp_group,
owns_attention=self.draft_owns_attention,
),
speculative_moe_backend_context(), speculative_moe_backend_context(),
): ):
super().init_cuda_graphs() super().init_cuda_graphs()
+1 -1
View File
@@ -361,7 +361,7 @@ class _ProfilerTorch(_ProfilerConcreteBase):
filename_parts = [self.profile_id, f"TP-{get_parallel().tp_rank}"] filename_parts = [self.profile_id, f"TP-{get_parallel().tp_rank}"]
# Only add other ranks if parallelism is enabled (size > 1) # Only add other ranks if parallelism is enabled (size > 1)
if self.ps.dp_size > 1: if get_parallel().dp_size > 1:
filename_parts.append(f"DP-{get_parallel().dp_rank}") filename_parts.append(f"DP-{get_parallel().dp_rank}")
if get_parallel().pp_size > 1: if get_parallel().pp_size > 1:
filename_parts.append(f"PP-{get_parallel().pp_rank}") filename_parts.append(f"PP-{get_parallel().pp_rank}")
@@ -23,13 +23,12 @@ from unittest.mock import MagicMock, patch
import msgspec.msgpack import msgspec.msgpack
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
from sglang.srt.managers.scheduler_components.load_publisher import ( from sglang.srt.managers.scheduler_components.load_publisher import (
LoadStat, LoadStat,
SchedulerLoadPublisher, SchedulerLoadPublisher,
) )
from sglang.test.ci.ci_register import register_cpu_ci from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase from sglang.test.test_utils import CustomTestCase, published_topology
register_cpu_ci(est_time=11, suite="base-a-test-cpu") register_cpu_ci(est_time=11, suite="base-a-test-cpu")
@@ -97,19 +96,21 @@ class TestLoadPublisherGating(CustomTestCase):
connect-style one. connect-style one.
""" """
def _build( def _build(self, *, config=ZMQ_ENDPOINT, explicit="auto", ranks=None, **topology):
self, *, config=ZMQ_ENDPOINT, dp_size=1, explicit="auto", **ps_overrides
):
"""Construct a publisher with the socket bind stubbed out, returning """Construct a publisher with the socket bind stubbed out, returning
(publisher, captured _open_pub_socket mock). Opts in via explicit="auto" (publisher, captured _open_pub_socket mock). Opts in via explicit="auto"
by default (the feature is off without it). dp_size lives on the ps, by default (the feature is off without it). The topology is published
which the publisher reads (no separate param to disagree with it).""" rather than overridden, so the ranks the publisher reads are the ones a
with patch( layout of that shape actually produces; every read happens in the
"sglang.srt.managers.scheduler_components.load_publisher._open_pub_socket" constructor."""
) as open_sock: with (
published_topology(ranks=ranks, **topology),
patch(
"sglang.srt.managers.scheduler_components.load_publisher._open_pub_socket"
) as open_sock,
):
pub = SchedulerLoadPublisher( pub = SchedulerLoadPublisher(
kv_events_config=config, kv_events_config=config,
ps=ParallelState.trivial(dp_size=dp_size, **ps_overrides),
load_publish_endpoint=explicit, load_publish_endpoint=explicit,
) )
return pub, open_sock return pub, open_sock
@@ -130,14 +131,17 @@ class TestLoadPublisherGating(CustomTestCase):
def test_disabled_off_pp_rank_zero(self): def test_disabled_off_pp_rank_zero(self):
# Every PP stage shares attn_tp_rank/attn_cp_rank 0, so without the # Every PP stage shares attn_tp_rank/attn_cp_rank 0, so without the
# pp_rank gate they all bind the same load port. # pp_rank gate they all bind the same load port.
pub, open_sock = self._build(pp_rank=1, pp_size=2) pub, open_sock = self._build(pp_size=2, ranks={"world_rank": 1})
self.assertFalse(pub.enable) self.assertFalse(pub.enable)
open_sock.assert_not_called() open_sock.assert_not_called()
def test_disabled_off_attn_tp_and_cp_rank_zero(self): def test_disabled_off_attn_tp_and_cp_rank_zero(self):
for override in ({"attn_tp_rank": 1}, {"attn_cp_rank": 1}): for layout in (
with self.subTest(**override): {"tp_size": 2},
pub, open_sock = self._build(**override) {"tp_size": 2, "attn_cp_size": 2},
):
with self.subTest(**layout):
pub, open_sock = self._build(ranks={"world_rank": 1}, **layout)
self.assertFalse(pub.enable) self.assertFalse(pub.enable)
open_sock.assert_not_called() open_sock.assert_not_called()
@@ -145,11 +149,16 @@ class TestLoadPublisherGating(CustomTestCase):
# Pure DP: attn_dp_size == 1 and every worker has attn_dp_rank == 0, so # Pure DP: attn_dp_size == 1 and every worker has attn_dp_rank == 0, so
# the publisher must key off dp_rank or all replicas collide on one # the publisher must key off dp_rank or all replicas collide on one
# port. kv 5557 + dp_size 4 => base 5561; rank 2 binds 5563. # port. kv 5557 + dp_size 4 => base 5561; rank 2 binds 5563.
_, open_sock = self._build(attn_dp_size=1, attn_dp_rank=0, dp_rank=2, dp_size=4) _, open_sock = self._build(dp_size=4, ranks={"world_rank": 0, "dp_rank": 2})
open_sock.assert_called_once_with("tcp://*:5563") open_sock.assert_called_once_with("tcp://*:5563")
def test_dp_attention_keys_the_load_port_by_attn_dp_rank(self): def test_dp_attention_keys_the_load_port_by_attn_dp_rank(self):
_, open_sock = self._build(attn_dp_size=4, attn_dp_rank=3, dp_rank=0, dp_size=4) _, open_sock = self._build(
tp_size=4,
dp_size=4,
enable_dp_attention=True,
ranks={"world_rank": 3, "dp_rank": 0},
)
open_sock.assert_called_once_with("tcp://*:5564") open_sock.assert_called_once_with("tcp://*:5564")
def test_load_port_is_packed_after_the_kv_range(self): def test_load_port_is_packed_after_the_kv_range(self):
@@ -259,10 +268,8 @@ class TestLoadPublisherGating(CustomTestCase):
_, open_sock = self._build( _, open_sock = self._build(
explicit="tcp://*:7000", explicit="tcp://*:7000",
attn_dp_size=1,
attn_dp_rank=0,
dp_rank=2,
dp_size=4, dp_size=4,
ranks={"world_rank": 0, "dp_rank": 2},
) )
open_sock.assert_called_once_with("tcp://*:7002") open_sock.assert_called_once_with("tcp://*:7002")
@@ -289,11 +296,11 @@ class TestLoadPublisherGating(CustomTestCase):
"sglang.srt.managers.scheduler_components.load_publisher._open_pub_socket", "sglang.srt.managers.scheduler_components.load_publisher._open_pub_socket",
side_effect=zmq.ZMQError, side_effect=zmq.ZMQError,
) as open_sock: ) as open_sock:
pub = SchedulerLoadPublisher( with published_topology():
kv_events_config=ZMQ_ENDPOINT, pub = SchedulerLoadPublisher(
ps=ParallelState.trivial(), kv_events_config=ZMQ_ENDPOINT,
load_publish_endpoint="auto", load_publish_endpoint="auto",
) )
open_sock.assert_called_once() # the bind was attempted and failed open_sock.assert_called_once() # the bind was attempted and failed
self.assertFalse(pub.enable) self.assertFalse(pub.enable)
pub.publish_load_stat(MagicMock(), force=True) # still a no-op pub.publish_load_stat(MagicMock(), force=True) # still a no-op
@@ -468,11 +475,11 @@ class TestLoadStatIntegration(CustomTestCase):
with _socket.socket() as probe: with _socket.socket() as probe:
probe.bind(("", 0)) probe.bind(("", 0))
port = probe.getsockname()[1] port = probe.getsockname()[1]
pub = SchedulerLoadPublisher( with published_topology():
kv_events_config='{"publisher": "zmq", "endpoint": "tcp://*:5557"}', pub = SchedulerLoadPublisher(
ps=ParallelState.trivial(), kv_events_config='{"publisher": "zmq", "endpoint": "tcp://*:5557"}',
load_publish_endpoint=f"tcp://*:{port}", load_publish_endpoint=f"tcp://*:{port}",
) )
if pub.enable: if pub.enable:
break break
self.assertTrue(pub.enable, "load socket never bound a free port") self.assertTrue(pub.enable, "load socket never bound a free port")
@@ -91,7 +91,7 @@ def _request(feature, rid: str = "vlm-request") -> TokenizedEmbeddingReqInput:
) )
def _receiver(tp_size: int = 1) -> SchedulerRequestReceiver: def _receiver() -> SchedulerRequestReceiver:
group = SimpleNamespace(rank=0, ranks=[0], cpu_group=object()) group = SimpleNamespace(rank=0, ranks=[0], cpu_group=object())
return SchedulerRequestReceiver( return SchedulerRequestReceiver(
recv_from_tokenizer=None, recv_from_tokenizer=None,
@@ -99,14 +99,6 @@ def _receiver(tp_size: int = 1) -> SchedulerRequestReceiver:
recv_skipper=None, recv_skipper=None,
input_blocker=None, input_blocker=None,
mm_receiver=None, mm_receiver=None,
ps=SimpleNamespace(
pp_rank=0,
tp_size=tp_size,
attn_tp_rank=0,
attn_cp_rank=0,
attn_tp_size=1,
attn_cp_size=1,
),
tp_group=group, tp_group=group,
tp_cpu_group=group, tp_cpu_group=group,
attn_tp_group=group, attn_tp_group=group,
@@ -131,8 +123,8 @@ def _run_consensus_rank(rank: int, world_size: int, init_file: str) -> None:
) )
try: try:
req = _request(_failed_pointer() if rank == 1 else _successful_pointer()) req = _request(_failed_pointer() if rank == 1 else _successful_pointer())
parallel = SimpleNamespace(enable_dp_attention=False) parallel = SimpleNamespace(enable_dp_attention=False, tp_size=world_size)
receiver = _receiver(tp_size=world_size) receiver = _receiver()
object.__setattr__(receiver, "tp_cpu_group", torch.distributed.group.WORLD) object.__setattr__(receiver, "tp_cpu_group", torch.distributed.group.WORLD)
with ( with (
patch( patch(
@@ -161,7 +153,7 @@ def _run_image_receiver(rank, init_file, pipe):
backend="gloo", init_method=Path(init_file).as_uri(), rank=rank, world_size=2 backend="gloo", init_method=Path(init_file).as_uri(), rank=rank, world_size=2
) )
try: try:
receiver = _receiver(tp_size=2) receiver = _receiver()
object.__setattr__(receiver, "tp_cpu_group", torch.distributed.group.WORLD) object.__setattr__(receiver, "tp_cpu_group", torch.distributed.group.WORLD)
torch.distributed.barrier() torch.distributed.barrier()
torch.distributed.all_reduce(torch.zeros(1)) torch.distributed.all_reduce(torch.zeros(1))
@@ -179,7 +171,7 @@ def _run_image_receiver(rank, init_file, pipe):
), ),
patch( patch(
"sglang.srt.managers.scheduler_components.request_receiver.get_parallel", "sglang.srt.managers.scheduler_components.request_receiver.get_parallel",
return_value=SimpleNamespace(enable_dp_attention=False), return_value=SimpleNamespace(enable_dp_attention=False, tp_size=2),
), ),
): ):
for base in [30, 90]: for base in [30, 90]:
@@ -382,7 +374,7 @@ class TestShmRequestFailureConsensus(unittest.TestCase):
def test_local_materialization_failure_becomes_request_error(self): def test_local_materialization_failure_becomes_request_error(self):
req = _request(_failed_pointer()) req = _request(_failed_pointer())
parallel = SimpleNamespace(enable_dp_attention=False) parallel = SimpleNamespace(enable_dp_attention=False, tp_size=1)
with ( with (
patch( patch(
@@ -406,7 +398,7 @@ class TestShmRequestFailureConsensus(unittest.TestCase):
def test_peer_failure_rejects_the_local_request(self): def test_peer_failure_rejects_the_local_request(self):
req = _request(torch.zeros(1)) req = _request(torch.zeros(1))
parallel = SimpleNamespace(enable_dp_attention=False) parallel = SimpleNamespace(enable_dp_attention=False, tp_size=2)
def inject_peer_failure(mask, **kwargs): def inject_peer_failure(mask, **kwargs):
mask.fill_(1) mask.fill_(1)
@@ -429,7 +421,7 @@ class TestShmRequestFailureConsensus(unittest.TestCase):
side_effect=inject_peer_failure, side_effect=inject_peer_failure,
) as all_reduce, ) as all_reduce,
): ):
_receiver(tp_size=2)._finalize_shm_features([req]) _receiver()._finalize_shm_features([req])
all_reduce.assert_called_once() all_reduce.assert_called_once()
self.assertIsInstance(req.mm_inputs, MMInputsProcessError) self.assertIsInstance(req.mm_inputs, MMInputsProcessError)
@@ -438,7 +430,7 @@ class TestShmRequestFailureConsensus(unittest.TestCase):
failed_req = _request(torch.zeros(1), rid="failed") failed_req = _request(torch.zeros(1), rid="failed")
healthy_req = _request(torch.zeros(1), rid="healthy") healthy_req = _request(torch.zeros(1), rid="healthy")
batch = BatchTokenizedEmbeddingReqInput(batch=[failed_req, healthy_req]) batch = BatchTokenizedEmbeddingReqInput(batch=[failed_req, healthy_req])
parallel = SimpleNamespace(enable_dp_attention=False) parallel = SimpleNamespace(enable_dp_attention=False, tp_size=1)
def materialize(req): def materialize(req):
if req.rid == "failed": if req.rid == "failed":
@@ -14,7 +14,6 @@ from sglang.test.test_utils import (
maybe_stub_sgl_kernel() maybe_stub_sgl_kernel()
from sglang.srt.distributed.parallel_state_wrapper import ParallelState # noqa: E402
from sglang.srt.managers.scheduler_components.request_receiver import ( # noqa: E402 from sglang.srt.managers.scheduler_components.request_receiver import ( # noqa: E402
SchedulerRequestReceiver, SchedulerRequestReceiver,
) )
@@ -23,29 +22,12 @@ from sglang.srt.managers.scheduler_pp_mixin import SchedulerPPMixin # noqa: E40
register_cpu_ci(est_time=11, suite="base-a-test-cpu") register_cpu_ci(est_time=11, suite="base-a-test-cpu")
def _make_ps(**overrides) -> ParallelState:
defaults = dict(
tp_size=8,
pp_rank=1,
pp_size=2,
dp_rank=None,
attn_tp_size=2,
attn_cp_size=2,
attn_dp_rank=1,
attn_dp_size=2,
moe_dp_rank=None,
)
defaults.update(overrides)
return ParallelState.trivial(**defaults)
def _published_topology(): def _published_topology():
"""The topology `_make_ps` describes, published instead of stood in. """The topology these tests run in.
World rank 12 of a `tp=8, pp=2` world is `tp_rank=4` on the second stage, World rank 12 of a `tp=8, pp=2` world is `tp_rank=4` on the second stage,
which puts this process at `attn_dp_rank=1` with `attn_tp_rank=0`: the which puts this process at `attn_dp_rank=1` with `attn_tp_rank=0`: the
context derives all of them from that one number and the widths, where the context derives all of them from that one number and the widths.
record above had to be handed each.
""" """
return published_topology( return published_topology(
role="scheduler", role="scheduler",
@@ -62,7 +44,7 @@ def _fake_group() -> SimpleNamespace:
return SimpleNamespace(rank=0, ranks=[0], cpu_group=object()) return SimpleNamespace(rank=0, ranks=[0], cpu_group=object())
def _make_receiver(ps: ParallelState) -> SchedulerRequestReceiver: def _make_receiver() -> SchedulerRequestReceiver:
tp_group = _fake_group() tp_group = _fake_group()
attn_tp_group = _fake_group() attn_tp_group = _fake_group()
attn_cp_group = _fake_group() attn_cp_group = _fake_group()
@@ -73,7 +55,6 @@ def _make_receiver(ps: ParallelState) -> SchedulerRequestReceiver:
recv_skipper=None, recv_skipper=None,
input_blocker=None, input_blocker=None,
mm_receiver=None, mm_receiver=None,
ps=ps,
tp_group=tp_group, tp_group=tp_group,
tp_cpu_group=tp_group, tp_cpu_group=tp_group,
attn_tp_group=attn_tp_group, attn_tp_group=attn_tp_group,
@@ -97,19 +78,17 @@ class TestRequestReceiverBroadcast(unittest.TestCase):
# Decode uses pure DP attention (attn_tp=attn_cp=1). The DP controller # Decode uses pure DP attention (attn_tp=attn_cp=1). The DP controller
# sends control requests to every local leader, so no per-tick Gloo # sends control requests to every local leader, so no per-tick Gloo
# broadcast should remain in SchedulerRequestReceiver. # broadcast should remain in SchedulerRequestReceiver.
ps = SimpleNamespace( receiver = _make_receiver()
control_req = SimpleNamespace(kind="control")
parallel = SimpleNamespace(
enable_dp_attention=True,
enable_dp_attention_local_control_broadcast=True,
attn_tp_rank=0, attn_tp_rank=0,
attn_cp_rank=0, attn_cp_rank=0,
attn_tp_size=1, attn_tp_size=1,
attn_cp_size=1, attn_cp_size=1,
tp_size=32, tp_size=32,
) )
receiver = _make_receiver(ps)
control_req = SimpleNamespace(kind="control")
parallel = SimpleNamespace(
enable_dp_attention=True,
enable_dp_attention_local_control_broadcast=True,
)
with ( with (
patch( patch(
@@ -133,19 +112,17 @@ class TestRequestReceiverBroadcast(unittest.TestCase):
broadcast.assert_not_called() broadcast.assert_not_called()
def test_default_control_uses_full_tp_broadcast(self): def test_default_control_uses_full_tp_broadcast(self):
ps = SimpleNamespace( receiver = _make_receiver()
control_req = SimpleNamespace(kind="control")
parallel = SimpleNamespace(
enable_dp_attention=True,
enable_dp_attention_local_control_broadcast=False,
attn_tp_rank=0, attn_tp_rank=0,
attn_cp_rank=0, attn_cp_rank=0,
attn_tp_size=1, attn_tp_size=1,
attn_cp_size=1, attn_cp_size=1,
tp_size=32, tp_size=32,
) )
receiver = _make_receiver(ps)
control_req = SimpleNamespace(kind="control")
parallel = SimpleNamespace(
enable_dp_attention=True,
enable_dp_attention_local_control_broadcast=False,
)
with ( with (
patch( patch(
@@ -183,7 +160,6 @@ class TestRequestReceiverBroadcast(unittest.TestCase):
class TestPPCPRankOffsets(unittest.TestCase): class TestPPCPRankOffsets(unittest.TestCase):
def test_request_receiver_uses_cp_size_for_pp_recv_rank(self): def test_request_receiver_uses_cp_size_for_pp_recv_rank(self):
ps = _make_ps()
enter_scope(self, _published_topology()) enter_scope(self, _published_topology())
calls = [] calls = []
@@ -191,7 +167,7 @@ class TestPPCPRankOffsets(unittest.TestCase):
calls.append((rank, src, dst)) calls.append((rank, src, dst))
return ["req"] return ["req"]
receiver = _make_receiver(ps) receiver = _make_receiver()
with patch( with patch(
"sglang.srt.managers.scheduler_components.request_receiver." "sglang.srt.managers.scheduler_components.request_receiver."
"point_to_point_pyobj", "point_to_point_pyobj",
@@ -202,10 +178,8 @@ class TestPPCPRankOffsets(unittest.TestCase):
self.assertEqual(calls, [(12, 4, 12)]) self.assertEqual(calls, [(12, 4, 12)])
def test_pp_mixin_uses_cp_size_for_pyobj_send_and_recv_rank(self): def test_pp_mixin_uses_cp_size_for_pyobj_send_and_recv_rank(self):
ps = _make_ps()
enter_scope(self, _published_topology()) enter_scope(self, _published_topology())
scheduler = SchedulerPPMixin() scheduler = SchedulerPPMixin()
scheduler.ps = ps
scheduler.world_group = _fake_group() scheduler.world_group = _fake_group()
scheduler.attn_tp_group = _fake_group() scheduler.attn_tp_group = _fake_group()
scheduler.attn_tp_cpu_group = _fake_group() scheduler.attn_tp_cpu_group = _fake_group()
@@ -2338,6 +2338,119 @@ class TestWhoAnswersDuringADraftScope(CustomTestCase):
self.assertEqual(get_parallel().pp_size, 2) self.assertEqual(get_parallel().pp_size, 2)
self.assertEqual(get_parallel().pp_rank, 1) self.assertEqual(get_parallel().pp_rank, 1)
def _group(self, world_size, rank):
from sglang.srt.distributed.parallel_state import GroupCoordinator
group = GroupCoordinator.__new__(GroupCoordinator)
group.world_size = world_size
group.rank_in_group = rank
return group
def test_the_tensor_swap_states_the_draft_has_no_attention_replica(self):
"""The draft runs the whole model on the group being installed. Its
attention identity is therefore that group, with one replica -- while
the target this process also serves is attention-DP over four ranks."""
from sglang.srt.distributed import parallel_state
reset_context()
self.addCleanup(reset_context)
publish(
ServerArgs(
model_path="dummy", tp_size=4, dp_size=2, enable_dp_attention=True
),
role="scheduler",
ranks=SpawnRanks(world_rank=0, dp_rank=0),
)
self.assertEqual(get_parallel().attn_dp_size, 2)
self.assertEqual(get_parallel().attn_tp_size, 2)
group = self._group(world_size=2, rank=1)
with patch.object(parallel_state, "_TP", group):
with parallel_state.patch_tensor_parallel_group(group, owns_attention=True):
parallel = get_parallel()
self.assertEqual(parallel.tp_size, 2)
self.assertEqual(parallel.attn_tp_size, 2)
self.assertEqual(parallel.attn_tp_rank, 1)
self.assertEqual(parallel.attn_dp_size, 1)
self.assertEqual(parallel.attn_dp_rank, 0)
self.assertEqual(parallel.attn_cp_size, 1)
self.assertEqual(parallel.attn_cp_rank, 0)
# `dp_size` is the deployment's replica count, not a property
# of the group being installed, so the scope leaves it alone --
# `require_mlp_tp_gather` asserts on it under dp attention.
self.assertEqual(parallel.dp_size, 2)
# The whole point of stating the rest: the identity the
# override path and the group build both check holds in here.
self.assertEqual(
parallel.tp_size,
parallel.attn_tp_size
* parallel.attn_dp_size
* parallel.attn_cp_size,
)
self.assertEqual(get_parallel().attn_dp_size, 2)
self.assertEqual(get_parallel().dp_size, 2)
def test_every_caller_says_whether_the_draft_owns_its_attention(self):
"""The scope cannot work it out from the group it is handed: the same
call site passes an attention-TP slice for one draft and the target's
whole TP group for another, and the two want opposite answers. So the
worker states it, and a caller that forgets is the bug this catches --
`owns_attention` has no default, but a missing one is a TypeError only
on the path that runs, and these paths need a GPU and a draft model."""
import ast
package = _pathlib.Path(next(iter(_sglang.__path__))).resolve()
checkout = package.parents[1]
roots = [package] + [
checkout / name for name in ("test",) if (checkout / name).is_dir()
]
missing = []
for path in (q for root in roots for q in root.rglob("*.py")):
try:
tree = ast.parse(path.read_text(encoding="utf-8"))
except (SyntaxError, UnicodeDecodeError):
continue
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
name = getattr(func, "attr", None) or getattr(func, "id", None)
if name not in ("draft_tp_context", "patch_tensor_parallel_group"):
continue
if not any(kw.arg == "owns_attention" for kw in node.keywords):
missing.append(f"{path}:{node.lineno}")
self.assertEqual(missing, [], "these enter the draft scope without saying")
def test_a_full_width_swap_leaves_the_attention_layout_alone(self):
"""The other caller. A draft built outside any scope carries the
target's whole TP group, and the graph capture installs *that* -- so
the process is still one of two attention-DP replicas and still gathers
with the other one. Narrowing here would claim a replica count it does
not have, and the reader that acts on it is a collective: the DP gather
takes its buffer size from the replica count and its communicator from
this group, so the two stop agreeing and the all-gather is refused."""
from sglang.srt.distributed import parallel_state
reset_context()
self.addCleanup(reset_context)
publish(
ServerArgs(
model_path="dummy", tp_size=4, dp_size=2, enable_dp_attention=True
),
role="scheduler",
ranks=SpawnRanks(world_rank=0, dp_rank=0),
)
whole_tp = self._group(world_size=4, rank=0)
with patch.object(parallel_state, "_TP", whole_tp):
with parallel_state.patch_tensor_parallel_group(
whole_tp, owns_attention=False
):
parallel = get_parallel()
self.assertEqual(parallel.tp_size, 4)
self.assertEqual(parallel.attn_dp_size, 2)
self.assertEqual(parallel.attn_tp_size, 2)
self.assertEqual(parallel.dp_size, 2)
def test_a_report_built_for_a_runner_follows_that_runner(self): def test_a_report_built_for_a_runner_follows_that_runner(self):
"""A weight check is an on-demand request served from the scheduler """A weight check is an on-demand request served from the scheduler
loop, so it runs outside the scope that describes a draft runner. Its loop, so it runs outside the scope that describes a draft runner. Its