[Refactor] Clean up parallel runtime comments (#40632)
This commit is contained in:
@@ -327,7 +327,6 @@ def load_model(server_args, port_args, gpu_id, tp_rank):
|
|||||||
server_args=server_args,
|
server_args=server_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Phase two: this entry has no scheduler to run it.
|
|
||||||
bootstrap.init_parallel_runtime(
|
bootstrap.init_parallel_runtime(
|
||||||
server_args=server_args,
|
server_args=server_args,
|
||||||
model_config=model_config,
|
model_config=model_config,
|
||||||
|
|||||||
@@ -102,8 +102,6 @@ def _sync_srt_world_group() -> None:
|
|||||||
if srt_parallel_state._WORLD is None:
|
if srt_parallel_state._WORLD is None:
|
||||||
srt_parallel_state._WORLD = _WORLD
|
srt_parallel_state._WORLD = _WORLD
|
||||||
if srt_parallel_state._WORLD is _WORLD:
|
if srt_parallel_state._WORLD is _WORLD:
|
||||||
# On the context too: that is where a handle is read from, and
|
|
||||||
# assigning the module global above does not reach it.
|
|
||||||
get_parallel().override_permanently(world_group=_WORLD)
|
get_parallel().override_permanently(world_group=_WORLD)
|
||||||
|
|
||||||
|
|
||||||
@@ -115,18 +113,11 @@ def _clear_srt_world_group() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _sync_srt_tp_group() -> None:
|
def _sync_srt_tp_group() -> None:
|
||||||
"""Lend this package's TP group to `srt`, and state the widths it implies.
|
"""Expose this package's TP group, widths, and ranks to shared SRT layers.
|
||||||
|
|
||||||
Shared `srt` layers run in this package and ask `get_parallel()` for how to
|
Use the group's actual width: sequence parallelism can make it wider than
|
||||||
shard -- `srt/layers/attention/vision.py` reads `attn_tp_size`. The
|
the TP size in the dummy SRT configuration. Other parallel dimensions are
|
||||||
published `srt` config cannot answer: `gpu_worker.py` publishes a dummy
|
one. Overrides also work before SRT configuration is published.
|
||||||
carrying *this* package's `tp_size`, which a sequence-parallel launch sets
|
|
||||||
to 1 while the group lent here is as wide as the world. So the widths are
|
|
||||||
permanently overridden alongside the group -- this runs with no `srt`
|
|
||||||
config published at all, which is exactly why it cannot go through
|
|
||||||
`RuntimeContext.override` (it requires one).
|
|
||||||
|
|
||||||
Only tensor parallelism folds this way, so every other dimension is one.
|
|
||||||
"""
|
"""
|
||||||
import sglang.srt.distributed.parallel_state as srt_parallel_state
|
import sglang.srt.distributed.parallel_state as srt_parallel_state
|
||||||
from sglang.srt.runtime_context import derive_parallel_widths, get_parallel
|
from sglang.srt.runtime_context import derive_parallel_widths, get_parallel
|
||||||
@@ -137,21 +128,9 @@ def _sync_srt_tp_group() -> None:
|
|||||||
srt_parallel_state._ATTN_TP = _TP
|
srt_parallel_state._ATTN_TP = _TP
|
||||||
if srt_parallel_state._ATTN_TP is _TP:
|
if srt_parallel_state._ATTN_TP is _TP:
|
||||||
get_parallel().override_permanently(
|
get_parallel().override_permanently(
|
||||||
# The group itself, because that is what the `srt` context answers
|
|
||||||
# a handle with -- assigning the module global above does not reach
|
|
||||||
# it. `tp_size` comes with them: the group is as wide as the world
|
|
||||||
# while the dummy carries this package's, and the widths below are
|
|
||||||
# quotients of one number, so stating a subset would describe a
|
|
||||||
# layout that does not exist.
|
|
||||||
tp_group=_TP,
|
tp_group=_TP,
|
||||||
attn_tp_group=_TP,
|
attn_tp_group=_TP,
|
||||||
tp_size=_TP.world_size,
|
tp_size=_TP.world_size,
|
||||||
# The ranks too. The shared layers shard by them -- `vision.py`
|
|
||||||
# reads `attn_tp_rank`, every `srt` linear built without an
|
|
||||||
# explicit rank reads `tp_rank` -- and this package publishes no
|
|
||||||
# rank bundle, so nothing else writes one. The draft has no
|
|
||||||
# pipeline, context or expert dimension of its own, so those
|
|
||||||
# positions are zero.
|
|
||||||
tp_rank=_TP.rank_in_group,
|
tp_rank=_TP.rank_in_group,
|
||||||
attn_tp_rank=_TP.rank_in_group,
|
attn_tp_rank=_TP.rank_in_group,
|
||||||
moe_tp_rank=_TP.rank_in_group,
|
moe_tp_rank=_TP.rank_in_group,
|
||||||
@@ -178,8 +157,7 @@ def _clear_srt_tp_group() -> None:
|
|||||||
srt_parallel_state._ATTN_TP = None
|
srt_parallel_state._ATTN_TP = None
|
||||||
get_parallel().clear_stamp()
|
get_parallel().clear_stamp()
|
||||||
if srt_parallel_state._WORLD is not None:
|
if srt_parallel_state._WORLD is not None:
|
||||||
# `clear_stamp` drops every stamped name; the WORLD group this
|
# Restore the still-active WORLD handle after clearing TP overrides.
|
||||||
# package lent is still built, so hand it back.
|
|
||||||
get_parallel().override_permanently(world_group=srt_parallel_state._WORLD)
|
get_parallel().override_permanently(world_group=srt_parallel_state._WORLD)
|
||||||
if srt_parallel_state._TP is _TP:
|
if srt_parallel_state._TP is _TP:
|
||||||
srt_parallel_state._TP = None
|
srt_parallel_state._TP = None
|
||||||
|
|||||||
@@ -148,13 +148,8 @@ def test_srt_attention_tp_group_tracks_diffusion_tp_group():
|
|||||||
assert srt_parallel_state._TP is tp_group
|
assert srt_parallel_state._TP is tp_group
|
||||||
assert srt_parallel_state._ATTN_TP is tp_group
|
assert srt_parallel_state._ATTN_TP is tp_group
|
||||||
assert get_parallel().attn_tp_size == 2
|
assert get_parallel().attn_tp_size == 2
|
||||||
# The handle too: assigning the module global does not reach the `srt`
|
|
||||||
# context, which is what the shared layers ask for a group.
|
|
||||||
assert get_parallel().tp_group is tp_group
|
assert get_parallel().tp_group is tp_group
|
||||||
assert get_parallel().attn_tp_group is tp_group
|
assert get_parallel().attn_tp_group is tp_group
|
||||||
# And the ranks the shared layers shard by. Nothing else writes one
|
|
||||||
# here: this package publishes no rank bundle, so a handle without a
|
|
||||||
# rank leaves every `srt` linear unable to say which shard it is.
|
|
||||||
assert get_parallel().tp_rank == 1
|
assert get_parallel().tp_rank == 1
|
||||||
assert get_parallel().attn_tp_rank == 1
|
assert get_parallel().attn_tp_rank == 1
|
||||||
|
|
||||||
|
|||||||
@@ -96,40 +96,18 @@ _NO_DEFAULT = object()
|
|||||||
|
|
||||||
|
|
||||||
class Derived(msgspec.Struct, frozen=True):
|
class Derived(msgspec.Struct, frozen=True):
|
||||||
"""Metadata for a field the configuration implies, not one anyone types.
|
"""Metadata for namespace fields that are not CLI inputs or record fields.
|
||||||
|
|
||||||
The other half of a namespace. An ``Arg`` field is the operator's input and
|
``fn`` is a lazily resolved dotted function path. It computes a value from
|
||||||
is collected into ``ServerArgs``; a ``Derived`` field carries no annotation,
|
resolved configuration once at publication. Fields without ``fn``, such as
|
||||||
so it is not a dataclass field and never reaches the record -- which is
|
ranks and group handles, are set at runtime. Parallel overrides take
|
||||||
right, because it has no input to preserve and the record is what crosses a
|
precedence over published values.
|
||||||
process boundary.
|
|
||||||
|
|
||||||
``fn`` names what computes it, as a dotted path resolved lazily so that a
|
|
||||||
declaration module stays free of runtime imports. Such a field is a pure
|
|
||||||
function of the published configuration, so it is computed once at
|
|
||||||
``publish`` and stored as an ordinary bag leaf -- a plain attribute load,
|
|
||||||
which is what a read inside compiled model code needs.
|
|
||||||
|
|
||||||
A declaration with no ``fn`` is one nothing can compute: a rank, or a
|
|
||||||
process group. Those are written into the namespace at runtime -- by
|
|
||||||
``publish`` from the spawn bundle, or by the build that creates the group --
|
|
||||||
and until then the name has no answer.
|
|
||||||
|
|
||||||
Most declarations carry ``fn``, the parallel quotients included:
|
|
||||||
they are a function of the configured leaves, so they are computed at
|
|
||||||
publish like the rest. What is special about them is not how they are
|
|
||||||
computed but that a stamp can move one afterwards -- ``initialize_dp_attention``
|
|
||||||
restamps ``attn_dp_size`` -- which ``ParallelContext`` answers above the
|
|
||||||
published leaf.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
doc: str = ""
|
doc: str = ""
|
||||||
fn: str = ""
|
fn: str = ""
|
||||||
# For a declaration with no ``fn`` whose absence is itself an answer:
|
# Default for runtime-only fields, e.g. ``gpu_id=None`` without a device.
|
||||||
# ``gpu_id`` is ``None`` in a process that runs on no device, and a reader
|
# Fields without a default raise if read before initialization.
|
||||||
# wants that rather than an error. A rank has no such value -- the wrong
|
|
||||||
# one is a hang in a collective -- so it carries no default and a read
|
|
||||||
# before the write says so.
|
|
||||||
default: Any = _NO_DEFAULT
|
default: Any = _NO_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -280,18 +280,7 @@ class Parallel(msgspec.Struct):
|
|||||||
"Maximum EP size the server can scale to at runtime. Pre-allocates active-rank state and backend buffers to this size. Defaults to the launch-time world size.",
|
"Maximum EP size the server can scale to at runtime. Pre-allocates active-rank state and backend buffers to this size. Defaults to the launch-time world size.",
|
||||||
] = None
|
] = None
|
||||||
|
|
||||||
# ---- derived: the quotients of the leaves above -------------------------
|
# Derived fields are computed at publication and are not stored in ServerArgs.
|
||||||
#
|
|
||||||
# Declared here, beside what they are computed from, because a namespace is
|
|
||||||
# one file and one class. They are not annotated, so they are not dataclass
|
|
||||||
# fields and `collect_input_fields` does not put them on the record -- which
|
|
||||||
# is right: a quotient has no operator input to preserve, and the record is
|
|
||||||
# what crosses a process boundary, so a width put there would be a stale
|
|
||||||
# copy the moment an elastic scale-up restamps one. Every input is a leaf
|
|
||||||
# above, so all six are fixed once the configuration is: `publish` computes
|
|
||||||
# them through `parallel_widths_of` and stores them as ordinary bag leaves,
|
|
||||||
# and `ParallelContext` answers with the stamp when a scale-up has moved
|
|
||||||
# one.
|
|
||||||
attn_tp_size = Derived(
|
attn_tp_size = Derived(
|
||||||
fn="sglang.srt.runtime_context.attn_tp_size_of",
|
fn="sglang.srt.runtime_context.attn_tp_size_of",
|
||||||
doc="Attention tensor-parallel width: `tp_size` divided by the "
|
doc="Attention tensor-parallel width: `tp_size` divided by the "
|
||||||
@@ -321,13 +310,7 @@ class Parallel(msgspec.Struct):
|
|||||||
"wider than one rank, which is exactly when the group gets built.",
|
"wider than one rank, which is exactly when the group gets built.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# -- written at runtime, not carried by any configuration --------------
|
# Runtime fields: publish sets ranks; distributed initialization sets groups.
|
||||||
#
|
|
||||||
# No `fn`: nothing here is a function of the leaves above. A rank is
|
|
||||||
# written by `publish` from the spawn bundle; a group by the build that
|
|
||||||
# creates it. Until one of them has run there is no answer, and a read
|
|
||||||
# says so rather than deriving something that would answer a different
|
|
||||||
# question.
|
|
||||||
tp_rank = Derived(doc="This process's place in the tensor-parallel group.")
|
tp_rank = Derived(doc="This process's place in the tensor-parallel group.")
|
||||||
pp_rank = Derived(doc="This process's place in the pipeline group.")
|
pp_rank = Derived(doc="This process's place in the pipeline group.")
|
||||||
moe_ep_rank = Derived(doc="This process's place in the expert-parallel group.")
|
moe_ep_rank = Derived(doc="This process's place in the expert-parallel group.")
|
||||||
|
|||||||
@@ -589,12 +589,7 @@ class MMEncoder:
|
|||||||
distributed_init_method=dist_init_method,
|
distributed_init_method=dist_init_method,
|
||||||
local_rank=rank,
|
local_rank=rank,
|
||||||
)
|
)
|
||||||
# The encoder serves the vision tower on a world of its own: `tp_size`
|
# The encoder uses a separate WORLD with tensor and attention-CP parallelism.
|
||||||
# ranks wide, with no pipeline, no expert or MoE-DP dimension and no
|
|
||||||
# decode context parallelism, whatever the generation side published.
|
|
||||||
# That has always been the layout it builds; stating it is what stops
|
|
||||||
# the context from answering with the other side's topology while these
|
|
||||||
# groups answer with this one.
|
|
||||||
parallel = get_parallel()
|
parallel = get_parallel()
|
||||||
attn_cp_size = parallel.attn_cp_size
|
attn_cp_size = parallel.attn_cp_size
|
||||||
attn_tp_size = parallel.tp_size // attn_cp_size
|
attn_tp_size = parallel.tp_size // attn_cp_size
|
||||||
|
|||||||
@@ -59,23 +59,18 @@ _is_cpu_arm64 = is_host_cpu_arm64()
|
|||||||
_TP_ALL_TO_ALL_WARMUP_BYTES_PER_PEER = 4 << 20
|
_TP_ALL_TO_ALL_WARMUP_BYTES_PER_PEER = 4 << 20
|
||||||
|
|
||||||
|
|
||||||
#: Set by `init_parallel`; `destroy_model_parallel` clears it, so a test that
|
#: Cleared by `destroy_model_parallel`.
|
||||||
#: tears the groups down can build them again.
|
|
||||||
_PARALLEL_INITIALISED = False
|
_PARALLEL_INITIALISED = False
|
||||||
|
|
||||||
|
|
||||||
def reset_parallel_initialised() -> None:
|
def reset_parallel_initialised() -> None:
|
||||||
"""Forget that the groups were built. Paired with tearing them down."""
|
"""Reset the initialization guard when model-parallel groups are destroyed."""
|
||||||
global _PARALLEL_INITIALISED
|
global _PARALLEL_INITIALISED
|
||||||
_PARALLEL_INITIALISED = False
|
_PARALLEL_INITIALISED = False
|
||||||
|
|
||||||
|
|
||||||
def _bind_threads_if_cpu(*, device: str) -> "Optional[List[int]]":
|
def _bind_threads_if_cpu(*, device: str) -> "Optional[List[int]]":
|
||||||
"""Pin OpenMP threads to this process's NUMA node, on CPU.
|
"""Bind OpenMP threads to the NUMA node before CPU group initialization."""
|
||||||
|
|
||||||
A precondition of the CPU group build, which reads the binding, so it is
|
|
||||||
done here rather than left for a caller to remember.
|
|
||||||
"""
|
|
||||||
if device != "cpu":
|
if device != "cpu":
|
||||||
return None
|
return None
|
||||||
from sglang.srt.utils import numa_utils
|
from sglang.srt.utils import numa_utils
|
||||||
@@ -99,23 +94,11 @@ def init_parallel_runtime(
|
|||||||
device: str,
|
device: str,
|
||||||
dist_port: int,
|
dist_port: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Phase two of startup: bring the parallel runtime up, once.
|
"""Initialize the parallel runtime once, after publishing configuration.
|
||||||
|
|
||||||
Publish says what the topology is; this makes it exist. Nothing returns,
|
Set up CPU thread binding, the current device, and the shared Mooncake
|
||||||
because the groups are read through the runtime context -- a caller that
|
engine before creating process groups. Draft workers reuse their target's
|
||||||
wants one asks `get_parallel()`, in this process or any later phase.
|
groups and must not call this function.
|
||||||
|
|
||||||
"Runtime" rather than "groups": two things have to be in place before the
|
|
||||||
groups can be built, and they are done here rather than left for every
|
|
||||||
entry to remember. The OpenMP/NUMA binding is what the CPU group build
|
|
||||||
reads, and the shared Mooncake transfer engine is what the Mooncake
|
|
||||||
process-group backend asks for -- create that one late and a second engine
|
|
||||||
appears. Both are preconditions of the build, not separate work.
|
|
||||||
|
|
||||||
Runs on the target worker only. A draft worker shares its target's groups,
|
|
||||||
which is why this is a phase the entry runs rather than something a runner
|
|
||||||
does on its way up: whether the groups exist must not depend on which
|
|
||||||
runner happened to be constructed first.
|
|
||||||
"""
|
"""
|
||||||
global _PARALLEL_INITIALISED
|
global _PARALLEL_INITIALISED
|
||||||
if _PARALLEL_INITIALISED:
|
if _PARALLEL_INITIALISED:
|
||||||
@@ -140,9 +123,7 @@ def init_parallel_runtime(
|
|||||||
_set_all_reduce_flags()
|
_set_all_reduce_flags()
|
||||||
|
|
||||||
local_omp_cpuid = _bind_threads_if_cpu(device=device)
|
local_omp_cpuid = _bind_threads_if_cpu(device=device)
|
||||||
# Everything below allocates on the current device -- the NCCL warm-up, the
|
# Select the local device before communicator allocation and warmup.
|
||||||
# mooncake all-reduce buffer -- and without this every rank on a node would
|
|
||||||
# pick device 0, because the default is not to reindex the visible set.
|
|
||||||
try:
|
try:
|
||||||
torch.get_device_module(device).set_device(get_device().gpu_id)
|
torch.get_device_module(device).set_device(get_device().gpu_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -199,11 +180,9 @@ def init_parallel_runtime(
|
|||||||
|
|
||||||
|
|
||||||
def measure_pre_model_load_memory(*, device: str, is_draft_worker: bool) -> float:
|
def measure_pre_model_load_memory(*, device: str, is_draft_worker: bool) -> float:
|
||||||
"""Available memory after the groups exist and before the model loads.
|
"""Measure available memory for KV-cache sizing before this runner loads.
|
||||||
|
|
||||||
Sized into the KV cache later, so it has to be taken at exactly this point
|
Call after parallel initialization and before allocating model weights.
|
||||||
-- which is why it stays with the runner rather than moving into the
|
|
||||||
parallel phase.
|
|
||||||
"""
|
"""
|
||||||
before_avail_memory = get_available_gpu_memory(device, get_device().gpu_id)
|
before_avail_memory = get_available_gpu_memory(device, get_device().gpu_id)
|
||||||
|
|
||||||
|
|||||||
@@ -2156,12 +2156,10 @@ _PDMUX_PREFILL_TP_GROUP: Optional[GroupCoordinator] = None
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def pdmux_prefill_tp_group():
|
def pdmux_prefill_tp_group():
|
||||||
"""Run on the prefill stream's own tensor-parallel communicator.
|
"""Use the duplicate TP communicator for the prefill stream.
|
||||||
|
|
||||||
PD multiplexing builds a duplicate TP group -- the same ranks, a second
|
PD multiplexing keeps prefill and decode on separate communicators with
|
||||||
communicator -- so prefill and decode can occupy separate streams without
|
the same ranks. Only the TP handle changes within this scope.
|
||||||
serialising on one. Nothing about the topology differs, so the scope states
|
|
||||||
the handle and nothing else.
|
|
||||||
"""
|
"""
|
||||||
assert _PDMUX_PREFILL_TP_GROUP is not None, (
|
assert _PDMUX_PREFILL_TP_GROUP is not None, (
|
||||||
"tensor model parallel group for PD-Multiplexing Prefill is not initialized"
|
"tensor model parallel group for PD-Multiplexing Prefill is not initialized"
|
||||||
@@ -2503,9 +2501,7 @@ def init_distributed_environment(
|
|||||||
assert _WORLD.world_size == torch.distributed.get_world_size(), (
|
assert _WORLD.world_size == torch.distributed.get_world_size(), (
|
||||||
"world group already initialized with a different world size"
|
"world group already initialized with a different world size"
|
||||||
)
|
)
|
||||||
# Stated here rather than with the groups below it: WORLD is built in this
|
# Publish WORLD before model-parallel initialization reads its local rank.
|
||||||
# function, and every group `initialize_model_parallel` builds is placed by
|
|
||||||
# reading it back.
|
|
||||||
get_parallel().override_permanently(world_group=_WORLD)
|
get_parallel().override_permanently(world_group=_WORLD)
|
||||||
|
|
||||||
|
|
||||||
@@ -2520,17 +2516,8 @@ def initialize_model_parallel(
|
|||||||
"""
|
"""
|
||||||
Initialize model parallel groups at the published widths.
|
Initialize model parallel groups at the published widths.
|
||||||
|
|
||||||
Every width comes from the runtime context rather than from an argument:
|
Read topology widths from ``get_parallel()``. Callers needing a different
|
||||||
the configuration already says how wide each dimension is, and a caller
|
layout must override the context before building groups.
|
||||||
that translates it again is a second place for the two to disagree. A
|
|
||||||
process that needs a narrower layout than the one it published -- the
|
|
||||||
media encoder is the case in the tree -- states that layout on the context
|
|
||||||
first, so what it builds and what it answers stay the same thing.
|
|
||||||
|
|
||||||
The remaining arguments are not topology. `backend` is decided by the
|
|
||||||
device, `duplicate_tp_group` and `enable_symm_mem` by other namespaces, and
|
|
||||||
`recovered_rank` / `rank_offset` / `max_world_size` describe this
|
|
||||||
particular join rather than the layout being joined.
|
|
||||||
|
|
||||||
The widths this reads:
|
The widths this reads:
|
||||||
tp_size: GPUs used for tensor model parallelism.
|
tp_size: GPUs used for tensor model parallelism.
|
||||||
@@ -2935,19 +2922,8 @@ def initialize_model_parallel(
|
|||||||
max_world_size=max_world_size,
|
max_world_size=max_world_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
# The groups just built and the configuration they were built from are two
|
# Validate group widths against the context. Leave disabled groups unset
|
||||||
# accounts of one layout, and this is where they meet: stating a group
|
# so reading them raises. WORLD was published by distributed initialization.
|
||||||
# checks the identities, so a group built on the wrong peers is refused
|
|
||||||
# here rather than hanging in a collective later.
|
|
||||||
#
|
|
||||||
# A dimension this configuration does not have is left unstated -- `_DCP`
|
|
||||||
# is None without decode context parallelism -- so reading it says the
|
|
||||||
# group was never built, which is what these getters have always said,
|
|
||||||
# rather than handing back a None to fail on at the collective.
|
|
||||||
#
|
|
||||||
# WORLD is not here: it is built and stated by
|
|
||||||
# `init_distributed_environment`, which is what lets every build above
|
|
||||||
# place its group by reading `get_world_group().local_rank`.
|
|
||||||
built = {
|
built = {
|
||||||
"tp_group": _TP,
|
"tp_group": _TP,
|
||||||
"pp_group": _PP,
|
"pp_group": _PP,
|
||||||
@@ -3061,8 +3037,6 @@ def patch_pipeline_parallel_group(pp_group: GroupCoordinator):
|
|||||||
old_pp_group = _PP
|
old_pp_group = _PP
|
||||||
_PP = pp_group
|
_PP = pp_group
|
||||||
try:
|
try:
|
||||||
# `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(
|
with get_parallel().override(
|
||||||
pp_size=pp_group.world_size,
|
pp_size=pp_group.world_size,
|
||||||
pp_rank=pp_group.rank_in_group,
|
pp_rank=pp_group.rank_in_group,
|
||||||
@@ -3076,36 +3050,12 @@ def patch_pipeline_parallel_group(pp_group: GroupCoordinator):
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def patch_tensor_parallel_group(tp_group: GroupCoordinator, *, owns_attention: bool):
|
def patch_tensor_parallel_group(tp_group: GroupCoordinator, *, owns_attention: bool):
|
||||||
"""Run under a different tensor-parallel group until this scope ends.
|
"""Temporarily replace the TP group and its runtime-context values.
|
||||||
|
|
||||||
This is for draft workers of speculative decoding, which run the draft model
|
For speculative drafts with ``owns_attention=True``, the installed group
|
||||||
at the target's attention-TP width rather than its global TP width.
|
is the draft's attention-TP group, with attention-DP, attention-CP, and
|
||||||
|
MoE-DP/EP widths set to one. Otherwise, retain the target's attention
|
||||||
The scope replaces both the module global that ``get_tp_group()`` reads and
|
topology. The worker must specify this based on how it constructed the draft.
|
||||||
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:
|
|
||||||
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
|
||||||
@@ -3458,20 +3408,9 @@ def monkey_patch_vllm_parallel_state(reverse: bool = False):
|
|||||||
setattr(vllm_parallel_state, "get_world_group", get_world_group)
|
setattr(vllm_parallel_state, "get_world_group", get_world_group)
|
||||||
|
|
||||||
|
|
||||||
# --- deprecation ---------------------------------------------------------
|
# Use `get_parallel()` outside this package. Warn once per deprecated getter.
|
||||||
#
|
|
||||||
# These getters are the definition of a name, not a second spelling of it.
|
|
||||||
# Business code asks `get_parallel()`, which answers by calling them and which
|
|
||||||
# a scope can redirect; a call that arrives here directly cannot be redirected,
|
|
||||||
# so a draft worker's scope does not reach it. The package that defines them
|
|
||||||
# keeps calling them -- a read there would go through the context back into
|
|
||||||
# itself -- so the warning fires only for callers outside it, and once per
|
|
||||||
# name, because the point is to name the replacement rather than to fill a log.
|
|
||||||
_EXEMPT_CALLERS = ("sglang.srt.distributed.",)
|
_EXEMPT_CALLERS = ("sglang.srt.distributed.",)
|
||||||
|
|
||||||
# Which context name each getter here answers. The shim's own bookkeeping --
|
|
||||||
# what a getter was replaced by is of no interest to whoever declares the field
|
|
||||||
# -- so it is written next to the warning that uses it.
|
|
||||||
_CONTEXT_NAME_OF = {
|
_CONTEXT_NAME_OF = {
|
||||||
"get_world_group": "world_group",
|
"get_world_group": "world_group",
|
||||||
"get_tp_group": "tp_group",
|
"get_tp_group": "tp_group",
|
||||||
@@ -3494,13 +3433,8 @@ _CONTEXT_NAME_OF = {
|
|||||||
"get_attn_context_model_parallel_rank": "attn_cp_rank",
|
"get_attn_context_model_parallel_rank": "attn_cp_rank",
|
||||||
"get_dcp_rank": "dcp_rank",
|
"get_dcp_rank": "dcp_rank",
|
||||||
}
|
}
|
||||||
# The width getters read a built group; the context answers the same names from
|
# Only deprecate width getters whose group widths are validated against
|
||||||
# the configuration. Those are one answer rather than two only for the groups
|
# configuration by `_WIDTH_AND_GROUP` in `runtime_context`.
|
||||||
# the build checks against the configuration -- `_WIDTH_AND_GROUP` in
|
|
||||||
# `runtime_context` -- so only those are listed here. `moe_dp`, `moe_tp` and
|
|
||||||
# `dcp` are not on that list and are deliberately absent: the MoE-DP group is
|
|
||||||
# the attention-CP group when the latter is wider, and the other two are simply
|
|
||||||
# not pinned yet.
|
|
||||||
_CONTEXT_NAME_OF["get_tensor_model_parallel_world_size"] = "tp_size"
|
_CONTEXT_NAME_OF["get_tensor_model_parallel_world_size"] = "tp_size"
|
||||||
_CONTEXT_NAME_OF["get_attn_tensor_model_parallel_world_size"] = "attn_tp_size"
|
_CONTEXT_NAME_OF["get_attn_tensor_model_parallel_world_size"] = "attn_tp_size"
|
||||||
_CONTEXT_NAME_OF["get_attn_context_model_parallel_world_size"] = "attn_cp_size"
|
_CONTEXT_NAME_OF["get_attn_context_model_parallel_world_size"] = "attn_cp_size"
|
||||||
@@ -3540,9 +3474,7 @@ del _name, _replacement, _fn
|
|||||||
|
|
||||||
|
|
||||||
# What `from sglang.srt.distributed import *` re-exports: everything public
|
# What `from sglang.srt.distributed import *` re-exports: everything public
|
||||||
# except the deprecated getters. Business code reaches them through
|
# except the deprecated getters.
|
||||||
# `get_parallel()`, and the package that defines them imports them from this
|
|
||||||
# module by name, so nothing needs the package path to reach one.
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
_public
|
_public
|
||||||
for _public in list(globals())
|
for _public in list(globals())
|
||||||
|
|||||||
@@ -45,12 +45,10 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
def deployment_attn_dp_size() -> int:
|
def deployment_attn_dp_size() -> int:
|
||||||
"""Attention-DP replicas in the deployment, which no draft scope narrows.
|
"""Return the deployment's attention-DP replica count.
|
||||||
|
|
||||||
A draft runs on one attention-DP replica and its scope says so, but the
|
Draft scopes retain this count because their metadata gathers include
|
||||||
metadata a draft gathers is shaped by the replicas it gathers *with* --
|
the target's replicas.
|
||||||
the target's. Those come from the configuration, which the scope leaves
|
|
||||||
alone, so this answers the same number inside the scope and outside it.
|
|
||||||
"""
|
"""
|
||||||
parallel = get_parallel()
|
parallel = get_parallel()
|
||||||
attn_dp_size, _ = derive_attention_widths(
|
attn_dp_size, _ = derive_attention_widths(
|
||||||
@@ -63,25 +61,20 @@ def deployment_attn_dp_size() -> int:
|
|||||||
|
|
||||||
|
|
||||||
def dp_gather_width() -> int:
|
def dp_gather_width() -> int:
|
||||||
"""How many replicas the DP sync gathers over.
|
"""Return the DP gather width.
|
||||||
|
|
||||||
The attention-DP replicas, except after an elastic-EP scale-up, when the
|
After elastic scale-up, the gather spans the expanded WORLD; otherwise
|
||||||
gather spans the expanded WORLD -- whose width is the `dp_size` the
|
it spans the attention-DP replicas.
|
||||||
scale-up published. Read from the context either way: a scoped width has
|
|
||||||
to reach this, which is the whole reason the name has one home.
|
|
||||||
"""
|
"""
|
||||||
parallel = get_parallel()
|
parallel = get_parallel()
|
||||||
return parallel.dp_size if world_dp_gather_enabled() else parallel.attn_dp_size
|
return parallel.dp_size if world_dp_gather_enabled() else parallel.attn_dp_size
|
||||||
|
|
||||||
|
|
||||||
def dp_gather_slot() -> int:
|
def dp_gather_slot() -> int:
|
||||||
"""This process's index in the list the DP sync just gathered.
|
"""Return this process's index in the DP gather.
|
||||||
|
|
||||||
The gather spans the attention-DP replicas, except after an elastic-EP
|
After elastic scale-up, use the TP rank plus the join offset; otherwise
|
||||||
scale-up, when it spans the expanded WORLD and the joining cohort is
|
use the attention-DP rank.
|
||||||
numbered from its offset. Which list was gathered is what the flag below
|
|
||||||
says, so the index is read from there rather than kept as a second name on
|
|
||||||
the topology.
|
|
||||||
"""
|
"""
|
||||||
parallel = get_parallel()
|
parallel = get_parallel()
|
||||||
if world_dp_gather_enabled():
|
if world_dp_gather_enabled():
|
||||||
@@ -100,12 +93,10 @@ def enable_joiner_all_gather():
|
|||||||
|
|
||||||
|
|
||||||
def update_dp_attention_post_scale(new_dp_size: int, new_dp_rank: int):
|
def update_dp_attention_post_scale(new_dp_size: int, new_dp_rank: int):
|
||||||
"""Point the DP gather at the expanded WORLD.
|
"""Switch DP gathers to the expanded WORLD.
|
||||||
|
|
||||||
The widths themselves are not written here: the caller scales `dp_size` on
|
The caller updates the configured widths; these arguments identify the
|
||||||
the published bag, and the gather reads its width and this process's slot
|
scale-up in the log.
|
||||||
from there. The arguments are the values the caller is about to publish,
|
|
||||||
kept so the log says which scale-up this was.
|
|
||||||
"""
|
"""
|
||||||
get_flags().dp.use_world_group_for_gather = True
|
get_flags().dp.use_world_group_for_gather = True
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -433,9 +424,6 @@ def initialize_dp_attention(
|
|||||||
if ep_scale_joiner_of(resolving_view(server_args)):
|
if ep_scale_joiner_of(resolving_view(server_args)):
|
||||||
dp.joiner_skip_all_gather = True
|
dp.joiner_skip_all_gather = True
|
||||||
|
|
||||||
# Stamped together, after the elastic adjustment: the width and the rank
|
|
||||||
# describe one topology, and a reader that caught them mid-update would
|
|
||||||
# see this process placed in a group it is not in.
|
|
||||||
get_parallel().override_permanently(
|
get_parallel().override_permanently(
|
||||||
attn_dp_size=attn_dp_size, attn_dp_rank=attn_dp_rank
|
attn_dp_size=attn_dp_size, attn_dp_rank=attn_dp_rank
|
||||||
)
|
)
|
||||||
@@ -457,9 +445,6 @@ def is_allocation_symmetric() -> bool:
|
|||||||
|
|
||||||
def get_dp_local_info(forward_batch: ForwardBatch) -> Tuple[torch.Tensor, torch.Tensor]:
|
def get_dp_local_info(forward_batch: ForwardBatch) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
# `get_dp_local_info` is only called in global DP gather and scatter. We use global DP rank here.
|
# `get_dp_local_info` is only called in global DP gather and scatter. We use global DP rank here.
|
||||||
# The slot in the list that was gathered. A scale-up widens that list
|
|
||||||
# to WORLD, and this process's index in it is not its index among the
|
|
||||||
# launch replicas.
|
|
||||||
dp_rank = dp_gather_slot()
|
dp_rank = dp_gather_slot()
|
||||||
|
|
||||||
if forward_batch.dp_local_start_pos is None:
|
if forward_batch.dp_local_start_pos is None:
|
||||||
@@ -484,9 +469,6 @@ def get_dp_local_slice_cpu(
|
|||||||
# CPU (start, length) slice for DP-local data in a rank-padded buffer.
|
# CPU (start, length) slice for DP-local data in a rank-padded buffer.
|
||||||
# Returns Python ints (no D2H sync) and handles the cuda-graph-padded layout.
|
# Returns Python ints (no D2H sync) and handles the cuda-graph-padded layout.
|
||||||
global_num_tokens = forward_batch.global_num_tokens_cpu
|
global_num_tokens = forward_batch.global_num_tokens_cpu
|
||||||
# The slot in the list that was gathered. A scale-up widens that list
|
|
||||||
# to WORLD, and this process's index in it is not its index among the
|
|
||||||
# launch replicas.
|
|
||||||
dp_rank = dp_gather_slot()
|
dp_rank = dp_gather_slot()
|
||||||
local_num_tokens = global_num_tokens[dp_rank]
|
local_num_tokens = global_num_tokens[dp_rank]
|
||||||
if can_run_graph:
|
if can_run_graph:
|
||||||
|
|||||||
@@ -112,10 +112,7 @@ class Sampler(nn.Module):
|
|||||||
self.cp_sync_group = None
|
self.cp_sync_group = None
|
||||||
if is_dp_attention_enabled():
|
if is_dp_attention_enabled():
|
||||||
self.tp_sync_group = get_parallel().attn_tp_group.device_group
|
self.tp_sync_group = get_parallel().attn_tp_group.device_group
|
||||||
# Only when there is more than one context shard to reconcile. The
|
# Single-shard drafts may have no context-parallel group.
|
||||||
# sync below already short-circuits on that, and a model running on
|
|
||||||
# one shard -- a speculative draft, under the scope that says so --
|
|
||||||
# has no context-parallel communicator to name.
|
|
||||||
if get_parallel().attn_cp_size > 1:
|
if get_parallel().attn_cp_size > 1:
|
||||||
self.cp_sync_group = get_parallel().attn_cp_group.device_group
|
self.cp_sync_group = get_parallel().attn_cp_group.device_group
|
||||||
|
|
||||||
|
|||||||
@@ -517,8 +517,6 @@ class Scheduler(
|
|||||||
# Init ZBAL, switch allocator should before any torch alloc action
|
# Init ZBAL, switch allocator should before any torch alloc action
|
||||||
self.init_zbal_on_npu()
|
self.init_zbal_on_npu()
|
||||||
|
|
||||||
# The groups are the first thing that allocates, so this comes after the
|
|
||||||
# allocator switch above and before anything that reads a group.
|
|
||||||
bootstrap.init_parallel_runtime(
|
bootstrap.init_parallel_runtime(
|
||||||
server_args=server_args,
|
server_args=server_args,
|
||||||
model_config=self.model_config,
|
model_config=self.model_config,
|
||||||
@@ -5910,7 +5908,6 @@ def dispatch_event_loop(scheduler: Scheduler):
|
|||||||
|
|
||||||
|
|
||||||
def _dispatch_event_loop_once(scheduler: Scheduler):
|
def _dispatch_event_loop_once(scheduler: Scheduler):
|
||||||
# The live PP property asserts before torch.distributed init (MLX stub).
|
|
||||||
disaggregation_mode: DisaggregationMode = scheduler.disaggregation_mode
|
disaggregation_mode: DisaggregationMode = scheduler.disaggregation_mode
|
||||||
if disaggregation_mode == DisaggregationMode.NULL:
|
if disaggregation_mode == DisaggregationMode.NULL:
|
||||||
if scheduler.enable_pdmux:
|
if scheduler.enable_pdmux:
|
||||||
@@ -5940,15 +5937,8 @@ def _dispatch_event_loop_once(scheduler: Scheduler):
|
|||||||
|
|
||||||
|
|
||||||
def resolve_spawn_dp_rank(dp_rank: Optional[int]) -> Optional[int]:
|
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.
|
"""Resolve the launcher DP rank, falling back to ``SGLANG_DP_RANK``."""
|
||||||
|
|
||||||
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:
|
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 int(os.environ["SGLANG_DP_RANK"])
|
||||||
return dp_rank
|
return dp_rank
|
||||||
|
|
||||||
@@ -6034,10 +6024,6 @@ def run_scheduler_process(
|
|||||||
# Load plugins so hooks can override Scheduler and its dependencies.
|
# Load plugins so hooks can override Scheduler and its dependencies.
|
||||||
load_plugins()
|
load_plugins()
|
||||||
dp_rank = resolve_spawn_dp_rank(dp_rank)
|
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(
|
publish(
|
||||||
server_args,
|
server_args,
|
||||||
role="scheduler",
|
role="scheduler",
|
||||||
|
|||||||
@@ -59,9 +59,7 @@ def _resolve_elastic_world_dp_size(
|
|||||||
|
|
||||||
live_dp_size = dp_gather_width()
|
live_dp_size = dp_gather_width()
|
||||||
effective_ep_size = ElasticEPStateManager.get_effective_ep_size()
|
effective_ep_size = ElasticEPStateManager.get_effective_ep_size()
|
||||||
# The group's own membership, not the width it was built at: this is the
|
# Query live membership because elastic joins can expand WORLD.
|
||||||
# one number an out-of-process join moves, and it is the upper bound the
|
|
||||||
# served width has to stay under.
|
|
||||||
world_size = torch.distributed.get_world_size(group)
|
world_size = torch.distributed.get_world_size(group)
|
||||||
|
|
||||||
if live_dp_size != effective_ep_size:
|
if live_dp_size != effective_ep_size:
|
||||||
|
|||||||
@@ -267,8 +267,6 @@ def build_kv_cache(
|
|||||||
enable_hierarchical_cache: bool,
|
enable_hierarchical_cache: bool,
|
||||||
hicache_draft_plan: Optional[HiCacheDraftPlan] = None,
|
hicache_draft_plan: Optional[HiCacheDraftPlan] = None,
|
||||||
) -> KVCacheBuildResult:
|
) -> KVCacheBuildResult:
|
||||||
# Built from the scheduler loop, outside any draft scope, so the context
|
|
||||||
# answers for the process this cache belongs to.
|
|
||||||
parallel = get_parallel()
|
parallel = get_parallel()
|
||||||
sliding_window_size: Optional[int] = None
|
sliding_window_size: Optional[int] = None
|
||||||
full_tokens_per_layer: Optional[int] = None
|
full_tokens_per_layer: Optional[int] = None
|
||||||
|
|||||||
@@ -259,8 +259,7 @@ class _PoolSizes(msgspec.Struct, frozen=True, kw_only=True):
|
|||||||
class KVCacheConfigurator:
|
class KVCacheConfigurator:
|
||||||
device: str
|
device: str
|
||||||
gpu_id: int
|
gpu_id: int
|
||||||
# Frozen at construction, not asked for later: this configurator is built
|
# Capture draft placement at construction; the configurator outlives the scope.
|
||||||
# inside the scope that describes a draft runner and used outside it.
|
|
||||||
attn_dp_size: int
|
attn_dp_size: int
|
||||||
pp_size: int
|
pp_size: int
|
||||||
pp_group: Any
|
pp_group: Any
|
||||||
|
|||||||
@@ -46,13 +46,9 @@ def host_memory_budget_scope(budget_bytes: int):
|
|||||||
|
|
||||||
|
|
||||||
def ranks_per_host() -> int:
|
def ranks_per_host() -> int:
|
||||||
"""Number of ranks of this job running on the same machine as this one.
|
"""Return the launch ranks per host, assuming uniform placement.
|
||||||
|
|
||||||
Derived as the launch width // nnodes: the launcher slices ranks
|
Avoid a collective: ranks may construct different numbers of host pools.
|
||||||
uniformly across nodes (resolution asserts divisibility), so no hostname
|
|
||||||
collective is needed — a collective here would have to be issued the same
|
|
||||||
number of times on every rank, and ranks build different numbers of host
|
|
||||||
pools.
|
|
||||||
"""
|
"""
|
||||||
if not (torch.distributed.is_available() and torch.distributed.is_initialized()):
|
if not (torch.distributed.is_available() and torch.distributed.is_initialized()):
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -1157,11 +1157,7 @@ class ModelRunner:
|
|||||||
self.pre_model_load_memory = bootstrap.measure_pre_model_load_memory(
|
self.pre_model_load_memory = bootstrap.measure_pre_model_load_memory(
|
||||||
device=self.device, is_draft_worker=self.is_draft_worker
|
device=self.device, is_draft_worker=self.is_draft_worker
|
||||||
)
|
)
|
||||||
# Read once, here: a draft runner is constructed inside the scope that
|
# Capture draft placement at construction; the runner outlives the scope.
|
||||||
# states its topology and used outside it, so what it holds has to be
|
|
||||||
# the placement it was built for rather than whatever the context
|
|
||||||
# answers later. Groups and widths alike -- a runner asked about its own
|
|
||||||
# shape after the scope has closed must still describe itself.
|
|
||||||
parallel = get_parallel()
|
parallel = get_parallel()
|
||||||
self.tp_group = parallel.tp_group
|
self.tp_group = parallel.tp_group
|
||||||
self.pp_group = parallel.pp_group
|
self.pp_group = parallel.pp_group
|
||||||
|
|||||||
@@ -671,12 +671,8 @@ class InklingSharedFusedMoE(FusedMoE):
|
|||||||
quant_config: QuantizationConfig | None,
|
quant_config: QuantizationConfig | None,
|
||||||
inference_moe_w13_interleaved: bool,
|
inference_moe_w13_interleaved: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
# FusedMoE.__init__ reads get_parallel() once and caches it on self, so
|
# FusedMoE caches this topology at construction. Shared experts are
|
||||||
# scoping the override to just this call is sufficient for the module's lifetime.
|
# replicated, so they need no expert-parallel group.
|
||||||
# The shared experts are replicated rather than sharded, so there is no
|
|
||||||
# expert-parallel communication here and no group to name: a width of
|
|
||||||
# one with the wider group still installed would describe a layout that
|
|
||||||
# does not exist.
|
|
||||||
with get_parallel().override(
|
with get_parallel().override(
|
||||||
moe_ep_size=1,
|
moe_ep_size=1,
|
||||||
moe_ep_rank=0,
|
moe_ep_rank=0,
|
||||||
|
|||||||
@@ -13,13 +13,10 @@
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
"""A single structured accessor for process-static runtime state.
|
"""A single structured accessor for process-static runtime state.
|
||||||
|
|
||||||
``get_parallel()`` returns a ``ParallelContext``. Ranks and process-group handles
|
``get_parallel()`` returns a ``ParallelContext`` for configuration, ranks, and
|
||||||
read through **live** to the canonical getter in ``distributed.parallel_state`` /
|
process-group handles. Reads use scoped overrides, then permanent overrides,
|
||||||
``layers.dp_attention`` — exactly what those getters return, a read-through
|
then the published configuration. ``publish`` records ranks from ``SpawnRanks``;
|
||||||
wrapper and not a cache. Every other name, the sizes included, is a leaf of the
|
distributed initialization records group handles.
|
||||||
published ``parallel`` bag. It gives call-sites one import and one naming scheme
|
|
||||||
in place of a dozen free functions, plus an ``override()`` hook to force a
|
|
||||||
topology without monkeypatching the underlying getters.
|
|
||||||
|
|
||||||
``get_server_args()`` returns the process-wide ``ServerArgs``. This is the
|
``get_server_args()`` returns the process-wide ``ServerArgs``. This is the
|
||||||
user's raw input, kept **read-only** for debug and reproduction; what
|
user's raw input, kept **read-only** for debug and reproduction; what
|
||||||
@@ -72,13 +69,7 @@ _PARALLEL_STATE = None
|
|||||||
|
|
||||||
|
|
||||||
def _ps():
|
def _ps():
|
||||||
"""The module every rank and group read ends at.
|
"""Lazily import and cache the parallel-state module."""
|
||||||
|
|
||||||
Cached because the import statement dominated the read: a group read is
|
|
||||||
two attribute lookups plus this, and it runs per row-linear on an eager
|
|
||||||
forward. The getter is still resolved by name on the returned module, so
|
|
||||||
a test that patches `parallel_state.get_tp_group` is still seen.
|
|
||||||
"""
|
|
||||||
global _PARALLEL_STATE
|
global _PARALLEL_STATE
|
||||||
if _PARALLEL_STATE is None:
|
if _PARALLEL_STATE is None:
|
||||||
from sglang.srt.distributed import parallel_state
|
from sglang.srt.distributed import parallel_state
|
||||||
@@ -95,11 +86,7 @@ def _dp():
|
|||||||
|
|
||||||
@functools.lru_cache(maxsize=1)
|
@functools.lru_cache(maxsize=1)
|
||||||
def _parallel_config_leaves() -> frozenset:
|
def _parallel_config_leaves() -> frozenset:
|
||||||
"""Names under the ``parallel`` namespace, for the unpublished error path.
|
"""Return configured parallel field names, including before publication."""
|
||||||
|
|
||||||
Read from the field metadata rather than the bag, which is what does not
|
|
||||||
exist yet when this is needed.
|
|
||||||
"""
|
|
||||||
from sglang.srt.arg_groups.arg_utils import namespace_of
|
from sglang.srt.arg_groups.arg_utils import namespace_of
|
||||||
from sglang.srt.server_args import ServerArgs
|
from sglang.srt.server_args import ServerArgs
|
||||||
|
|
||||||
@@ -110,31 +97,12 @@ def _parallel_config_leaves() -> frozenset:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Ranks and group handles: the names no configuration carries. This table is
|
|
||||||
# their declaration, the way `arg_groups/fields/parallel.py` is the leaves' and
|
|
||||||
# `Derived` is the widths'. A group handle names the getter that owns it,
|
|
||||||
# because the module that builds the groups is where it lives; a rank is a
|
|
||||||
# position in one of those groups, so it is read off the handle. `None` marks a
|
|
||||||
# name only a stamp can answer: no coordinator knows this process's
|
|
||||||
# attention-DP rank.
|
|
||||||
_MISSING_READ = object()
|
_MISSING_READ = object()
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=1)
|
@functools.lru_cache(maxsize=1)
|
||||||
def _parallel_fields() -> frozenset:
|
def _parallel_fields() -> frozenset:
|
||||||
"""Every name `ParallelContext` answers for, read from the declarations.
|
"""Return configured and derived parallel names accepted by overrides."""
|
||||||
|
|
||||||
Three sources, because the namespace has three kinds of name and each one
|
|
||||||
declares itself somewhere already:
|
|
||||||
|
|
||||||
* configured leaves -- the `parallel` namespace of the record;
|
|
||||||
* derived widths -- the `Derived` declarations beside those leaves;
|
|
||||||
* ranks and group handles -- declared beside the leaves with no `fn`,
|
|
||||||
because nothing computes them; they are written at runtime.
|
|
||||||
|
|
||||||
The set is the union of those three, so `override()` cannot refuse a name
|
|
||||||
the class answers for.
|
|
||||||
"""
|
|
||||||
from sglang.srt.arg_groups.arg_utils import Derived
|
from sglang.srt.arg_groups.arg_utils import Derived
|
||||||
from sglang.srt.arg_groups.fields.parallel import Parallel
|
from sglang.srt.arg_groups.fields.parallel import Parallel
|
||||||
|
|
||||||
@@ -147,12 +115,7 @@ def _parallel_fields() -> frozenset:
|
|||||||
def derive_attention_widths(
|
def derive_attention_widths(
|
||||||
*, tp_size: int, attn_cp_size: int, dp_size: int, enable_dp_attention: bool
|
*, tp_size: int, attn_cp_size: int, dp_size: int, enable_dp_attention: bool
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
"""(attn_dp_size, attn_tp_size) from the leaves.
|
"""Return (attn_dp_size, attn_tp_size) from the configured widths."""
|
||||||
|
|
||||||
Split out because the rank computation in
|
|
||||||
`dp_attention.compute_dp_attention_world_info` needs the same two numbers
|
|
||||||
and must not carry a second copy of the arithmetic.
|
|
||||||
"""
|
|
||||||
attn_dp_size = dp_size if enable_dp_attention else 1
|
attn_dp_size = dp_size if enable_dp_attention else 1
|
||||||
return attn_dp_size, tp_size // attn_dp_size // attn_cp_size
|
return attn_dp_size, tp_size // attn_dp_size // attn_cp_size
|
||||||
|
|
||||||
@@ -160,18 +123,12 @@ def derive_attention_widths(
|
|||||||
def derive_attention_ranks(
|
def derive_attention_ranks(
|
||||||
*, tp_rank: int, attn_tp_size: int, attn_cp_size: int, enable_dp_attention: bool
|
*, tp_rank: int, attn_tp_size: int, attn_cp_size: int, enable_dp_attention: bool
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
"""(attn_tp_rank, attn_dp_rank) for a process at `tp_rank`.
|
"""Return (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::
|
The rank layout is (dp, cp, tp), with tp changing fastest::
|
||||||
|
|
||||||
tp_rank = (attn_dp_rank * attn_cp_size + attn_cp_rank) * attn_tp_size
|
tp_rank = (attn_dp_rank * attn_cp_size + attn_cp_rank) * attn_tp_size
|
||||||
+ attn_tp_rank
|
+ 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
|
attn_tp_rank = tp_rank % attn_tp_size
|
||||||
if not enable_dp_attention:
|
if not enable_dp_attention:
|
||||||
@@ -180,13 +137,9 @@ def derive_attention_ranks(
|
|||||||
|
|
||||||
|
|
||||||
def spawn_world_rank(server_args, *, tp_rank: int, pp_rank: int) -> int:
|
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.
|
"""Compute the WORLD rank from launcher TP and PP ranks.
|
||||||
|
|
||||||
The inverse of `derive_spawn_ranks`, for the entries that have the pieces
|
Uses a resolving view because callers may run before ``publish``.
|
||||||
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
|
from sglang.srt.arg_groups.model_override_base import resolving_view
|
||||||
|
|
||||||
@@ -204,17 +157,10 @@ def derive_spawn_ranks(
|
|||||||
moe_dp_size: int,
|
moe_dp_size: int,
|
||||||
moe_ep_size: int,
|
moe_ep_size: int,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Every rank a process group would answer, from its place in WORLD.
|
"""Derive TP, PP, attention, and MoE ranks without process groups.
|
||||||
|
|
||||||
WORLD is laid out `rank = ep_join_rank_offset + tp_size * pp_rank +
|
WORLD uses ``rank = ep_join_rank_offset + tp_size * pp_rank + tp_rank``.
|
||||||
tp_rank`: `initialize_model_parallel` builds tensor-parallel groups as
|
TP groups are contiguous blocks; PP groups are strided by ``tp_size``.
|
||||||
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
|
local = world_rank - ep_join_rank_offset
|
||||||
tp_rank = local % tp_size
|
tp_rank = local % tp_size
|
||||||
@@ -242,27 +188,10 @@ def derive_parallel_widths(
|
|||||||
dcp_size: int,
|
dcp_size: int,
|
||||||
dcp_enabled: bool,
|
dcp_enabled: bool,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""The parallel widths no flag sets, from the leaves that do.
|
"""Derive attention and MoE widths and DCP settings from configuration."""
|
||||||
|
|
||||||
`tp_size` and its siblings are configured; these are quotients of them, so
|
|
||||||
the arithmetic lives here rather than being read back off the group
|
|
||||||
coordinators.
|
|
||||||
|
|
||||||
The world widths are not among them: neither is a quotient, and they are
|
|
||||||
not one number. `launch_world_size` is what the WORLD group was built at
|
|
||||||
and is frozen there -- `GroupCoordinator.world_size` is `len(ranks)`, so it
|
|
||||||
does not move when mooncake admits ranks into an expandable WORLD;
|
|
||||||
`max_world_size` is what that group has room for. How much of that room is
|
|
||||||
serving right now is elastic-EP state and is asked of its owner. Deriving
|
|
||||||
either from the leaves would be wrong in a further way: on a scale joiner it
|
|
||||||
would answer with the joining cohort's own `tp * pp`, while that process's
|
|
||||||
WORLD spans `ep_join_rank_offset + tp * pp`.
|
|
||||||
"""
|
|
||||||
return {
|
return {
|
||||||
"attn_dp_size": attn_dp_size,
|
"attn_dp_size": attn_dp_size,
|
||||||
# `attn_dp_size` is already the effective width (1 when DP attention is
|
# `attn_dp_size` already accounts for disabled DP attention.
|
||||||
# off), so the flag is spent here; a caller passing the raw `dp_size`
|
|
||||||
# leaf with the attention disabled would get tp/dp/cp instead of tp/1/cp.
|
|
||||||
"attn_tp_size": derive_attention_widths(
|
"attn_tp_size": derive_attention_widths(
|
||||||
tp_size=tp_size,
|
tp_size=tp_size,
|
||||||
attn_cp_size=attn_cp_size,
|
attn_cp_size=attn_cp_size,
|
||||||
@@ -277,14 +206,7 @@ def derive_parallel_widths(
|
|||||||
|
|
||||||
|
|
||||||
def parallel_widths_of(cfg: Any) -> dict:
|
def parallel_widths_of(cfg: Any) -> dict:
|
||||||
"""The six quotients, from a resolved config.
|
"""Return derived parallel settings from resolved configuration."""
|
||||||
|
|
||||||
Every input is a record field, so this is a function of the configuration
|
|
||||||
and nothing else -- which is why the six are declared `Derived(fn=...)` and
|
|
||||||
computed once at publish rather than on every read. `dcp_enabled` is
|
|
||||||
`dcp_size > 1` because that is exactly when `initialize_model_parallel`
|
|
||||||
builds the group.
|
|
||||||
"""
|
|
||||||
attn_dp_size, _ = derive_attention_widths(
|
attn_dp_size, _ = derive_attention_widths(
|
||||||
tp_size=cfg.tp_size,
|
tp_size=cfg.tp_size,
|
||||||
attn_cp_size=cfg.attn_cp_size,
|
attn_cp_size=cfg.attn_cp_size,
|
||||||
@@ -303,20 +225,15 @@ def parallel_widths_of(cfg: Any) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def launch_world_size_of(cfg: Any):
|
def launch_world_size_of(cfg: Any):
|
||||||
"""`launch_world_size`, computed at publish.
|
"""Return the initial WORLD width, including existing ranks below a joiner.
|
||||||
|
|
||||||
The width `bootstrap` builds the WORLD at: one rank per pipeline stage of
|
This value remains fixed after elastic scale-up.
|
||||||
each tensor-parallel group, above the offset a scale joiner comes in at --
|
|
||||||
zero for everyone else, which is the convention `spawn_world_rank` uses for
|
|
||||||
the same arithmetic. A scale-up does not move it, which is the point of the
|
|
||||||
name.
|
|
||||||
"""
|
"""
|
||||||
return cfg.ep_join_rank_offset + cfg.tp_size * cfg.pp_size
|
return cfg.ep_join_rank_offset + cfg.tp_size * cfg.pp_size
|
||||||
|
|
||||||
|
|
||||||
def max_world_size_of(cfg: Any):
|
def max_world_size_of(cfg: Any):
|
||||||
"""`max_world_size`, computed at publish. The ceiling the group is
|
"""Return the WORLD capacity: ``max_ep_size`` or the launch width."""
|
||||||
pre-allocated to: `--max-ep-size` when set, otherwise the launch width."""
|
|
||||||
return cfg.max_ep_size or launch_world_size_of(cfg)
|
return cfg.max_ep_size or launch_world_size_of(cfg)
|
||||||
|
|
||||||
|
|
||||||
@@ -351,25 +268,12 @@ def dcp_enabled_of(cfg: Any):
|
|||||||
|
|
||||||
|
|
||||||
class SpawnRanks(msgspec.Struct, frozen=True):
|
class SpawnRanks(msgspec.Struct, frozen=True):
|
||||||
"""Where the launcher put this process, in the two numbers only it knows.
|
"""Process placement supplied by the launcher.
|
||||||
|
|
||||||
`world_rank` is this process's place in the WORLD group, which fixes every
|
``world_rank`` determines TP, PP, attention, and MoE ranks from the
|
||||||
other rank: the groups are laid out from the published widths, so
|
configured widths. ``dp_rank`` identifies the replica across separate
|
||||||
`tp_rank`, `pp_rank` and the attention / MoE ranks are functions of it (see
|
WORLD groups; ``None`` means no data-parallel controller. ``gpu_id`` is
|
||||||
`derive_spawn_ranks`). Passing them separately would be passing the same
|
the assigned device index, or ``None`` for a process without a device.
|
||||||
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.
|
|
||||||
|
|
||||||
`gpu_id` is the device the parent picked for this process. It is not a
|
|
||||||
position in any group -- reindexing narrows the visible devices before the
|
|
||||||
spawn, and Ray allocates from its own pool -- but it is the same kind of
|
|
||||||
fact: something only the entry that spawned the process can state. `None`
|
|
||||||
for a process that runs on no device.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
world_rank: int
|
world_rank: int
|
||||||
@@ -386,10 +290,8 @@ _RANK_AND_WIDTH = (
|
|||||||
("moe_ep_rank", "moe_ep_size"),
|
("moe_ep_rank", "moe_ep_size"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# `moe_dp` is absent because `initialize_model_parallel` aliases the MoE-DP
|
# MoE-DP may alias a wider attention-CP group, so its configured width
|
||||||
# group to the attention-CP group when the latter is wider: there the group and
|
# need not match the group width.
|
||||||
# the name are two facts, which is the same reason `moe_dp_rank` is left off the
|
|
||||||
# record at publish.
|
|
||||||
_WIDTH_AND_GROUP = (
|
_WIDTH_AND_GROUP = (
|
||||||
("tp_size", "tp_group"),
|
("tp_size", "tp_group"),
|
||||||
("pp_size", "pp_group"),
|
("pp_size", "pp_group"),
|
||||||
@@ -402,25 +304,14 @@ _UNREADABLE = object()
|
|||||||
|
|
||||||
|
|
||||||
def _validate_parallel(parallel, source: str) -> None:
|
def _validate_parallel(parallel, source: str) -> None:
|
||||||
"""Fail on a topology that cannot describe a real process layout.
|
"""Check rank bounds, topology factorizations, and group widths.
|
||||||
|
|
||||||
Every identity holds unconditionally: a width and a rank are both
|
Skip unavailable or non-integer values so partially initialized contexts
|
||||||
plausible small integers whichever way they are wrong, so an inconsistent
|
can be validated.
|
||||||
set is not caught by anything downstream -- it surfaces as a hang or a
|
|
||||||
wrong answer in a collective, far from the write. Stating one leaf without
|
|
||||||
the quotients that follow from it leaves the namespace describing no real
|
|
||||||
layout, and the caller that did so is the one that has to say what it meant.
|
|
||||||
|
|
||||||
Names that cannot be read are skipped rather than treated as zero: a
|
|
||||||
process that has published nothing can still stamp a rank, and a group that
|
|
||||||
has not been built answers nothing at all.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def read(name):
|
def read(name):
|
||||||
"""A width or a rank, or `_UNREADABLE` for anything these identities
|
"""Return an integer topology value, or ``_UNREADABLE``; exclude booleans."""
|
||||||
cannot be stated about -- an absent name, `None`, or a stand-in a test
|
|
||||||
put in a group's place. Booleans are integers in Python and are not
|
|
||||||
widths, so they are out too."""
|
|
||||||
try:
|
try:
|
||||||
value = getattr(parallel, name)
|
value = getattr(parallel, name)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -500,24 +391,12 @@ def _validate_parallel(parallel, source: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
class ParallelContext:
|
class ParallelContext:
|
||||||
"""Parallel-topology namespace: one spelling per name.
|
"""Parallel configuration, process ranks, and group handles.
|
||||||
|
|
||||||
Every name is answered by a lookup, never by asking a process group. A
|
Configured and derived widths come from the published configuration.
|
||||||
configured leaf and a width derived from one come off the published
|
``publish`` records ranks from the launcher; distributed initialization
|
||||||
``parallel`` bag; a rank is written by ``publish`` from the spawn bundle,
|
records group handles. Scoped overrides take precedence over permanent
|
||||||
and a group handle by ``initialize_model_parallel`` as it builds them. A
|
overrides and configuration. Uninitialized runtime fields raise on read.
|
||||||
read before the write that answers it says which write is missing rather
|
|
||||||
than deriving a number from whatever is installed -- the two answer
|
|
||||||
different questions, and a plausible wrong rank surfaces as a hang in a
|
|
||||||
collective far from here.
|
|
||||||
|
|
||||||
That makes a scope a matter of stating names: ``patch_tensor_parallel_group``
|
|
||||||
runs a draft worker under a different TP group by overriding the members it
|
|
||||||
changes, and PD multiplexing points ``tp_group`` at the prefill
|
|
||||||
communicator the same way. Elastic EP needs no rule at all: it scales
|
|
||||||
``ep_size`` / ``dp_size`` on the published bag while ``launch_world_size``
|
|
||||||
keeps the width the groups were built at, so the two are different names
|
|
||||||
rather than two answers to one name.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_overrides", "_stamp", "_config")
|
__slots__ = ("_overrides", "_stamp", "_config")
|
||||||
@@ -536,17 +415,7 @@ class ParallelContext:
|
|||||||
return self._read(name)
|
return self._read(name)
|
||||||
|
|
||||||
def _read(self, name):
|
def _read(self, name):
|
||||||
"""The one read path, for every kind of name in the namespace.
|
"""Read a scoped override, permanent override, or published value, in order."""
|
||||||
|
|
||||||
Scoped override, then the permanent stamp, then the published bag.
|
|
||||||
A rank or a group handle is on no bag, so once those three are out the
|
|
||||||
name has not been written yet and the read says so.
|
|
||||||
|
|
||||||
The two override maps stay separate because they are taken down by
|
|
||||||
different things -- a `with` block and `clear_stamp()` -- and
|
|
||||||
merging them would let a teardown of one drop the other, and would
|
|
||||||
turn "which wins" into whichever was written last.
|
|
||||||
"""
|
|
||||||
overrides = self._overrides
|
overrides = self._overrides
|
||||||
if name in overrides:
|
if name in overrides:
|
||||||
return overrides[name]
|
return overrides[name]
|
||||||
@@ -577,20 +446,10 @@ class ParallelContext:
|
|||||||
raise AttributeError(f"ParallelContext has no {name!r}")
|
raise AttributeError(f"ParallelContext has no {name!r}")
|
||||||
|
|
||||||
def override_permanently(self, **values) -> None:
|
def override_permanently(self, **values) -> None:
|
||||||
"""Permanently record a width or rank the published bag can't answer
|
"""Set parallel values until ``clear_stamp`` or ``reset_context``.
|
||||||
or no longer answers correctly -- not `RuntimeContext.override`,
|
|
||||||
because neither is a resolved config leaf and this must work with no
|
|
||||||
config published at all (`multimodal_gen` lends a TP group to `srt`
|
|
||||||
layers with no `srt` config to publish against).
|
|
||||||
|
|
||||||
Widths are quotients of the configured leaves, so the bag can usually
|
Works without published configuration. Validate the combined topology
|
||||||
answer and this only corrects it; a rank is a per-process fact the
|
and restore the previous values if validation fails.
|
||||||
configuration never carries, so for those this is the only source.
|
|
||||||
|
|
||||||
Lives beside, not inside, the `@contextmanager` `override` below -- a
|
|
||||||
name it cannot also have on this class -- because these are permanent
|
|
||||||
for the process, not scoped to a `with` block: none of the real
|
|
||||||
callers ever restore the value they set here.
|
|
||||||
"""
|
"""
|
||||||
unknown = set(values) - _parallel_fields()
|
unknown = set(values) - _parallel_fields()
|
||||||
if unknown:
|
if unknown:
|
||||||
@@ -628,7 +487,7 @@ class ParallelContext:
|
|||||||
|
|
||||||
|
|
||||||
def _derived_widths() -> dict:
|
def _derived_widths() -> dict:
|
||||||
"""The declared quotients, by name -- `{name: Derived}`."""
|
"""Return parallel ``Derived`` declarations, including ranks and groups."""
|
||||||
from sglang.srt.arg_groups.arg_utils import Derived
|
from sglang.srt.arg_groups.arg_utils import Derived
|
||||||
from sglang.srt.arg_groups.fields.parallel import Parallel
|
from sglang.srt.arg_groups.fields.parallel import Parallel
|
||||||
|
|
||||||
@@ -638,18 +497,9 @@ def _derived_widths() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def _install_parallel_properties() -> None:
|
def _install_parallel_properties() -> None:
|
||||||
"""Give `ParallelContext` a property per name that is not a config leaf.
|
"""Expose declared parallel fields as documented properties.
|
||||||
|
|
||||||
Every name the namespace answers that is not a plain leaf: the quotients,
|
Properties support class-level introspection; all reads use ``_read``.
|
||||||
and the ranks and group handles declared beside them with no `fn`.
|
|
||||||
|
|
||||||
Properties rather than names left to `__getattr__` because the class
|
|
||||||
surface is what the guards introspect -- `hasattr(ParallelContext,
|
|
||||||
"tp_size")` and `vars(ParallelContext)` are how the tests read the set from
|
|
||||||
the class side -- and because each one carries its `Derived.doc`.
|
|
||||||
|
|
||||||
Every one of them resolves through `_read`, so there is a single priority
|
|
||||||
chain rather than one per kind of name.
|
|
||||||
"""
|
"""
|
||||||
for name, decl in _derived_widths().items():
|
for name, decl in _derived_widths().items():
|
||||||
|
|
||||||
@@ -755,10 +605,10 @@ class MoeFlags(_FlagGroupBase):
|
|||||||
|
|
||||||
|
|
||||||
class DpFlags(_FlagGroupBase):
|
class DpFlags(_FlagGroupBase):
|
||||||
"""DP-attention runtime flags, materialized by ``initialize_dp_attention``
|
"""DP-attention runtime flags set by ``initialize_dp_attention``.
|
||||||
(after distributed setup; reads the model config). The topology values it
|
|
||||||
also computes -- the attention-DP width and rank -- are stamped on
|
Attention-DP width and rank are stored on ``get_parallel()``.
|
||||||
``get_parallel()``, not kept here."""
|
"""
|
||||||
|
|
||||||
enabled: bool = False
|
enabled: bool = False
|
||||||
use_world_group_for_gather: bool = False
|
use_world_group_for_gather: bool = False
|
||||||
@@ -1134,9 +984,7 @@ def _install_derived_leaves(tops: dict, server_args: Any) -> None:
|
|||||||
if not isinstance(decl, Derived):
|
if not isinstance(decl, Derived):
|
||||||
continue
|
continue
|
||||||
if not decl.fn:
|
if not decl.fn:
|
||||||
# Nothing computes it. Seed the ones whose absence is itself an
|
# Initialize runtime-only fields that declare a default.
|
||||||
# answer, so a process that never states one still reads it;
|
|
||||||
# the rest stay unwritten and say so when read.
|
|
||||||
if decl.default is not _NO_DEFAULT:
|
if decl.default is not _NO_DEFAULT:
|
||||||
bag = tops.get(path.split(".")[0])
|
bag = tops.get(path.split(".")[0])
|
||||||
for segment in path.split(".")[1:]:
|
for segment in path.split(".")[1:]:
|
||||||
@@ -1262,10 +1110,7 @@ class RuntimeContext:
|
|||||||
# Snapshot resolved config into the namespace bags (the single source of
|
# Snapshot resolved config into the namespace bags (the single source of
|
||||||
# truth for config reads). Placed by `namespace_of`; a mock/partial
|
# truth for config reads). Placed by `namespace_of`; a mock/partial
|
||||||
# config that declares no namespace yields an empty tree (no bags).
|
# config that declares no namespace yields an empty tree (no bags).
|
||||||
# A name the configuration does not carry survives the re-projection.
|
# Preserve the launcher-assigned device when rebuilding config bags.
|
||||||
# `gpu_id` is stated by the spawn, not derived, so rebuilding the bags
|
|
||||||
# from the record must not unwrite it -- the record has no field for it
|
|
||||||
# to be rebuilt from.
|
|
||||||
stated = {}
|
stated = {}
|
||||||
if self._config_bags is not None:
|
if self._config_bags is not None:
|
||||||
device = self._config_bags.get("device")
|
device = self._config_bags.get("device")
|
||||||
@@ -1805,11 +1650,9 @@ def publish(
|
|||||||
namespace-read enforcement (``record`` audits the reads instead).
|
namespace-read enforcement (``record`` audits the reads instead).
|
||||||
``hf_config`` is accepted for forward-compat and currently unused.
|
``hf_config`` is accepted for forward-compat and currently unused.
|
||||||
|
|
||||||
``ranks`` is who this process is, from the entry that spawned it. It is
|
``ranks`` supplies launcher placement for roles that participate in the
|
||||||
optional because most roles are not placed in the topology at all -- a
|
parallel topology. Without it, rank reads require an explicit override,
|
||||||
tokenizer has no ``tp_rank`` -- and those processes raise on a rank read
|
except for ``attn_dcp_rank=0`` when DCP is disabled.
|
||||||
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
|
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**
|
engine running now. Re-publish is allowed and is **last-publish-wins**
|
||||||
@@ -1836,26 +1679,14 @@ def publish(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
_CONTEXT._publish_role = role
|
_CONTEXT._publish_role = role
|
||||||
# Zero for every process when decode context parallelism is off, which is a
|
# Disabled DCP has rank zero even in processes without a rank bundle.
|
||||||
# fact about the configuration and not about the spawn -- so it answers
|
|
||||||
# without a rank bundle, the way it did when it stood for a group that was
|
|
||||||
# never built. With DCP on it is a position, and the bundle below states it.
|
|
||||||
if not _CONTEXT.parallel.dcp_enabled:
|
if not _CONTEXT.parallel.dcp_enabled:
|
||||||
_CONTEXT.parallel.override_permanently(attn_dcp_rank=0)
|
_CONTEXT.parallel.override_permanently(attn_dcp_rank=0)
|
||||||
# Stated on the bag directly: `gpu_id` is declared but not configured, so
|
# The device is assigned by the launcher; it is not a config field.
|
||||||
# it is not a leaf `override` can route, and the spawn is the only thing
|
|
||||||
# that knows it. Written whatever it is, `None` included -- most roles run
|
|
||||||
# on no device, and that is an answer rather than a name nobody wrote.
|
|
||||||
_CONTEXT.config_bag("device")._set(
|
_CONTEXT.config_bag("device")._set(
|
||||||
"gpu_id", ranks.gpu_id if ranks is not None else None
|
"gpu_id", ranks.gpu_id if ranks is not None else None
|
||||||
)
|
)
|
||||||
if ranks is not None:
|
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
|
parallel = _CONTEXT.parallel
|
||||||
placement = derive_spawn_ranks(
|
placement = derive_spawn_ranks(
|
||||||
world_rank=ranks.world_rank,
|
world_rank=ranks.world_rank,
|
||||||
@@ -1866,30 +1697,18 @@ def publish(
|
|||||||
moe_dp_size=parallel.moe_dp_size,
|
moe_dp_size=parallel.moe_dp_size,
|
||||||
moe_ep_size=parallel.moe_ep_size,
|
moe_ep_size=parallel.moe_ep_size,
|
||||||
)
|
)
|
||||||
# `initialize_model_parallel` aliases the MoE-DP group to the
|
# MoE-DP aliases the attention-CP group when CP is wider.
|
||||||
# attention-CP one when the CP dimension is the wider of the two, so
|
|
||||||
# this process's place in it is its CP index rather than the MoE-DP
|
|
||||||
# index the arithmetic above gives.
|
|
||||||
if parallel.moe_dp_size < parallel.attn_cp_size:
|
if parallel.moe_dp_size < parallel.attn_cp_size:
|
||||||
placement["moe_dp_rank"] = placement["attn_cp_rank"]
|
placement["moe_dp_rank"] = placement["attn_cp_rank"]
|
||||||
# `dp_rank` is recorded whatever it is, None included: replicas are
|
# `None` means no data-parallel controller.
|
||||||
# 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["dp_rank"] = ranks.dp_rank
|
||||||
placement["launch_world_rank"] = ranks.world_rank
|
placement["launch_world_rank"] = ranks.world_rank
|
||||||
placement.update(_attention_ranks(parallel, placement["tp_rank"]))
|
placement.update(_attention_ranks(parallel, placement["tp_rank"]))
|
||||||
# A DCP group is a contiguous slice of a TP group, so this process's
|
# DCP groups are contiguous slices of a TP group.
|
||||||
# place in one is its TP rank folded by that width. `attn_dcp_rank` is
|
|
||||||
# the same number, and zero where decode context parallelism is off, so
|
|
||||||
# a reader does not have to ask whether it is on first.
|
|
||||||
if parallel.dcp_enabled:
|
if parallel.dcp_enabled:
|
||||||
placement["dcp_rank"] = placement["tp_rank"] % parallel.dcp_size
|
placement["dcp_rank"] = placement["tp_rank"] % parallel.dcp_size
|
||||||
placement["attn_dcp_rank"] = placement.get("dcp_rank", 0)
|
placement["attn_dcp_rank"] = placement.get("dcp_rank", 0)
|
||||||
# One stamp, not two: the identities are checked on every write, and a
|
|
||||||
# half-placed process satisfies none of them.
|
|
||||||
parallel.override_permanently(**placement)
|
parallel.override_permanently(**placement)
|
||||||
# Publish established the whole layout, so every identity applies here,
|
|
||||||
# not just the ones the stamp happened to name.
|
|
||||||
_validate_parallel(parallel, "publish")
|
_validate_parallel(parallel, "publish")
|
||||||
if _ROLE_NS_MODE == "record":
|
if _ROLE_NS_MODE == "record":
|
||||||
# The '-' marker distinguishes a zero-read role from a process where
|
# The '-' marker distinguishes a zero-read role from a process where
|
||||||
@@ -1906,18 +1725,7 @@ def publish(
|
|||||||
|
|
||||||
|
|
||||||
def _attention_ranks(parallel, tp_rank: int) -> dict:
|
def _attention_ranks(parallel, tp_rank: int) -> dict:
|
||||||
"""Place this process in the attention topology, from the configuration.
|
"""Derive attention ranks from the configured widths and the TP rank."""
|
||||||
|
|
||||||
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 the per-runner record used to provide
|
|
||||||
by being a plain frozen record.
|
|
||||||
|
|
||||||
These are stamped rather than written as bag leaves because they are
|
|
||||||
per-process facts, and nothing about the configuration distinguishes one
|
|
||||||
rank from another.
|
|
||||||
"""
|
|
||||||
attn_tp_rank, attn_dp_rank = derive_attention_ranks(
|
attn_tp_rank, attn_dp_rank = derive_attention_ranks(
|
||||||
tp_rank=tp_rank,
|
tp_rank=tp_rank,
|
||||||
attn_tp_size=parallel.attn_tp_size,
|
attn_tp_size=parallel.attn_tp_size,
|
||||||
@@ -2065,13 +1873,9 @@ def restore_context(state: dict[str, Any]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def reset_context() -> None:
|
def reset_context() -> None:
|
||||||
"""Clear the context-owned store (unit-test teardown): drop the published
|
"""Clear published configuration, parallel overrides, flags, and resources.
|
||||||
``server_args`` and install fresh ``Flags`` and ``Resources``.
|
|
||||||
|
|
||||||
``parallel`` holds the permanently-overridden derived widths, which go
|
Used for test teardown and runtime lifecycle reset.
|
||||||
with the lifecycle that set them: `_read` prefers them over the
|
|
||||||
published leaves, so leaving one behind lets the next test read the
|
|
||||||
previous topology.
|
|
||||||
"""
|
"""
|
||||||
_CONTEXT._server_args = None
|
_CONTEXT._server_args = None
|
||||||
_CONTEXT._config_bags = None
|
_CONTEXT._config_bags = None
|
||||||
|
|||||||
@@ -396,11 +396,7 @@ 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
|
||||||
)
|
)
|
||||||
# One decision, used twice: whether the draft runs on an attention-TP
|
# Use the same attention topology during draft construction and execution.
|
||||||
# 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
|
self.draft_owns_attention = get_parallel().enable_dp_attention
|
||||||
if self.draft_owns_attention:
|
if self.draft_owns_attention:
|
||||||
draft_init_ctx = draft_tp_context(
|
draft_init_ctx = draft_tp_context(
|
||||||
|
|||||||
@@ -260,12 +260,7 @@ class EagleDraftWorker(EagleDraftWorkerBase):
|
|||||||
|
|
||||||
self._rebuild_topk1_chain_buffers()
|
self._rebuild_topk1_chain_buffers()
|
||||||
|
|
||||||
# Load draft model weights only.
|
# Use the same attention topology during draft construction and execution.
|
||||||
# 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 = (
|
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()
|
||||||
|
|||||||
@@ -160,9 +160,7 @@ 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
|
# Retain the target's attention topology when swapping TP groups.
|
||||||
# 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_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
|
||||||
|
|||||||
@@ -188,10 +188,7 @@ class MultiLayerEagleDraftWorker(EagleDraftWorkerBase):
|
|||||||
"InklingForConditionalGenerationMTP",
|
"InklingForConditionalGenerationMTP",
|
||||||
"GigaChat35ForCausalLMNextN",
|
"GigaChat35ForCausalLMNextN",
|
||||||
]
|
]
|
||||||
# The draft runner is built outside any tensor-parallel scope, so it
|
# Retain the target's attention topology when swapping TP groups.
|
||||||
# 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_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
|
||||||
|
|||||||
@@ -88,10 +88,7 @@ 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
|
# Retain the target's attention topology when swapping TP groups.
|
||||||
# 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_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
|
||||||
|
|||||||
@@ -70,10 +70,7 @@ def _is_non_persistent_buffer_name(name: str) -> bool:
|
|||||||
class WeightChecker:
|
class WeightChecker:
|
||||||
def __init__(self, *, get_model: Callable[[], Any]):
|
def __init__(self, *, get_model: Callable[[], Any]):
|
||||||
self._get_model = get_model
|
self._get_model = get_model
|
||||||
# A check is served on demand from the scheduler loop, which is outside
|
# Capture the runner placement before its draft scope exits.
|
||||||
# the scope that describes a draft runner. The report has to name the
|
|
||||||
# runner it was built for, so the placement is read here, at
|
|
||||||
# construction, rather than asked for when the request arrives.
|
|
||||||
parallel = get_parallel()
|
parallel = get_parallel()
|
||||||
self._placement = ParallelismInfo(
|
self._placement = ParallelismInfo(
|
||||||
tp_rank=parallel.tp_rank,
|
tp_rank=parallel.tp_rank,
|
||||||
@@ -176,9 +173,7 @@ class WeightChecker:
|
|||||||
return info.model_dump()
|
return info.model_dump()
|
||||||
|
|
||||||
def _parallelism_info(self) -> ParallelismInfo:
|
def _parallelism_info(self) -> ParallelismInfo:
|
||||||
# The WORLD position is asked for now rather than frozen: unlike the
|
# Read the current WORLD rank because elastic scale-up can change it.
|
||||||
# runner's placement it is a property of the process, and an elastic
|
|
||||||
# scale-up moves it.
|
|
||||||
return self._placement.model_copy(
|
return self._placement.model_copy(
|
||||||
update={
|
update={
|
||||||
"rank": dist.get_rank() if dist.is_initialized() else 0,
|
"rank": dist.get_rank() if dist.is_initialized() else 0,
|
||||||
|
|||||||
@@ -22,11 +22,7 @@ from sglang.srt.model_executor.model_runner import ModelRunner
|
|||||||
from sglang.srt.runtime_context import get_context, get_parallel
|
from sglang.srt.runtime_context import get_context, get_parallel
|
||||||
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
|
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
|
||||||
|
|
||||||
# Unit tests run without distributed initialization. Backends that size buffers by
|
# Use single-rank attention without distributed initialization.
|
||||||
# attention tensor-parallel degree should see the single-rank default, and a
|
|
||||||
# backend that places itself in the decode context-parallel group needs a
|
|
||||||
# position: nothing publishes here, so there is no configuration to derive the
|
|
||||||
# zero these tests run at from.
|
|
||||||
_parallel_override = get_parallel().override(attn_tp_size=1, attn_dcp_rank=0)
|
_parallel_override = get_parallel().override(attn_tp_size=1, attn_dcp_rank=0)
|
||||||
_parallel_override.__enter__()
|
_parallel_override.__enter__()
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import torch
|
|||||||
import torch.nn.functional as F
|
import torch.nn.functional as F
|
||||||
from torch import nn
|
from torch import nn
|
||||||
|
|
||||||
# State the topology before importing modules that read it at __init__. The
|
# Set the single-rank topology before importing the attention implementation.
|
||||||
# group is stated too: `RowParallelLinear.forward` asks for it to manage
|
|
||||||
# symmetric memory, and `world_size=1` short-circuits that.
|
|
||||||
from sglang.srt.runtime_context import get_context, get_parallel
|
from sglang.srt.runtime_context import get_context, get_parallel
|
||||||
|
|
||||||
_parallel_override = get_parallel().override(
|
_parallel_override = get_parallel().override(
|
||||||
|
|||||||
@@ -2056,19 +2056,10 @@ def maybe_stub_sgl_kernel():
|
|||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def published_topology(role: str = "test", *, ranks=None, **server_args_fields):
|
def published_topology(role: str = "test", *, ranks=None, **server_args_fields):
|
||||||
"""Publish a record describing the parallel topology a test wants.
|
"""Publish a test topology, defaulting to WORLD rank zero.
|
||||||
|
|
||||||
Replaces standing a per-process parallel record into the object under
|
``ranks`` overrides the launcher placement. Reset the context before
|
||||||
test. The widths arrive the way production gets them -- from published
|
publication and on exit, including when the test fails.
|
||||||
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.runtime_context import SpawnRanks, publish, reset_context
|
||||||
from sglang.srt.server_args import ServerArgs
|
from sglang.srt.server_args import ServerArgs
|
||||||
@@ -2085,15 +2076,10 @@ def published_topology(role: str = "test", *, ranks=None, **server_args_fields):
|
|||||||
|
|
||||||
|
|
||||||
def publish_build_topology(*, world_rank: int = 0, **server_args_fields):
|
def publish_build_topology(*, world_rank: int = 0, **server_args_fields):
|
||||||
"""State the widths `initialize_model_parallel` is about to build at.
|
"""Publish the topology for a subsequent ``initialize_model_parallel`` call.
|
||||||
|
|
||||||
The build reads every width from the runtime context, so a test that wants
|
Preserve an existing WORLD group across the context reset. The caller is
|
||||||
a particular topology publishes it here rather than passing it in -- the
|
responsible for tearing down groups and resetting the context afterward.
|
||||||
same door production uses, which also keeps the derived widths honest.
|
|
||||||
|
|
||||||
Unlike `published_topology` this is not a scope: the groups it is about to
|
|
||||||
build outlive any block, so the configuration describing them has to as
|
|
||||||
well. Callers that tear the groups down are already resetting the process.
|
|
||||||
"""
|
"""
|
||||||
from sglang.srt.distributed import parallel_state
|
from sglang.srt.distributed import parallel_state
|
||||||
from sglang.srt.runtime_context import (
|
from sglang.srt.runtime_context import (
|
||||||
@@ -2110,11 +2096,7 @@ def publish_build_topology(*, world_rank: int = 0, **server_args_fields):
|
|||||||
role="test",
|
role="test",
|
||||||
ranks=SpawnRanks(world_rank=world_rank),
|
ranks=SpawnRanks(world_rank=world_rank),
|
||||||
)
|
)
|
||||||
# Callers that go on to build groups have already run
|
# Restore the existing WORLD handle after resetting the context.
|
||||||
# `init_distributed_environment`, which states the WORLD group -- and the
|
|
||||||
# build below places every group it creates by reading that back. The reset
|
|
||||||
# above drops it, so hand it over again: publishing a configuration does not
|
|
||||||
# unbuild a process group.
|
|
||||||
if parallel_state._WORLD is not None:
|
if parallel_state._WORLD is not None:
|
||||||
get_parallel().override_permanently(world_group=parallel_state._WORLD)
|
get_parallel().override_permanently(world_group=parallel_state._WORLD)
|
||||||
|
|
||||||
|
|||||||
@@ -131,8 +131,6 @@ def init_distributed():
|
|||||||
local_rank=local_rank,
|
local_rank=local_rank,
|
||||||
backend="nccl",
|
backend="nccl",
|
||||||
)
|
)
|
||||||
# The context answers a handle from what was stated on it, not from
|
|
||||||
# this module global, so a rank stood up by hand says so itself.
|
|
||||||
get_parallel().override_permanently(world_group=coord)
|
get_parallel().override_permanently(world_group=coord)
|
||||||
|
|
||||||
cpu_group = coord.cpu_group
|
cpu_group = coord.cpu_group
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ def _init_cpu_group() -> dist.ProcessGroup:
|
|||||||
local_rank=local_rank,
|
local_rank=local_rank,
|
||||||
backend="nccl",
|
backend="nccl",
|
||||||
)
|
)
|
||||||
# The context answers a handle from what was stated on it, not from
|
|
||||||
# this module global, so a rank stood up by hand says so itself.
|
|
||||||
get_parallel().override_permanently(world_group=coord)
|
get_parallel().override_permanently(world_group=coord)
|
||||||
atexit.register(dist.destroy_process_group)
|
atexit.register(dist.destroy_process_group)
|
||||||
torch.cuda.set_stream(torch.cuda.Stream())
|
torch.cuda.set_stream(torch.cuda.Stream())
|
||||||
|
|||||||
@@ -78,8 +78,6 @@ def _init_cpu_group() -> dist.ProcessGroup:
|
|||||||
local_rank=local_rank,
|
local_rank=local_rank,
|
||||||
backend="nccl",
|
backend="nccl",
|
||||||
)
|
)
|
||||||
# The context answers a handle from what was stated on it, not from
|
|
||||||
# this module global, so a rank stood up by hand says so itself.
|
|
||||||
get_parallel().override_permanently(world_group=ps._WORLD)
|
get_parallel().override_permanently(world_group=ps._WORLD)
|
||||||
atexit.register(dist.destroy_process_group)
|
atexit.register(dist.destroy_process_group)
|
||||||
logging.disable(logging.INFO)
|
logging.disable(logging.INFO)
|
||||||
|
|||||||
@@ -109,8 +109,6 @@ def _init_cpu_group() -> dist.ProcessGroup:
|
|||||||
local_rank=local_rank,
|
local_rank=local_rank,
|
||||||
backend="nccl",
|
backend="nccl",
|
||||||
)
|
)
|
||||||
# The context answers a handle from what was stated on it, not from
|
|
||||||
# this module global, so a rank stood up by hand says so itself.
|
|
||||||
get_parallel().override_permanently(world_group=coord)
|
get_parallel().override_permanently(world_group=coord)
|
||||||
atexit.register(dist.destroy_process_group)
|
atexit.register(dist.destroy_process_group)
|
||||||
logging.disable(logging.INFO)
|
logging.disable(logging.INFO)
|
||||||
|
|||||||
@@ -130,8 +130,6 @@ def _init_cpu_group_once() -> dist.ProcessGroup:
|
|||||||
local_rank=local_rank,
|
local_rank=local_rank,
|
||||||
backend="nccl",
|
backend="nccl",
|
||||||
)
|
)
|
||||||
# The context answers a handle from what was stated on it, not from
|
|
||||||
# this module global, so a rank stood up by hand says so itself.
|
|
||||||
get_parallel().override_permanently(world_group=coord)
|
get_parallel().override_permanently(world_group=coord)
|
||||||
atexit.register(dist.destroy_process_group)
|
atexit.register(dist.destroy_process_group)
|
||||||
cpu_group = coord.cpu_group
|
cpu_group = coord.cpu_group
|
||||||
|
|||||||
@@ -71,8 +71,6 @@ def _init_cpu_group_once() -> dist.ProcessGroup:
|
|||||||
local_rank=local_rank,
|
local_rank=local_rank,
|
||||||
backend="nccl",
|
backend="nccl",
|
||||||
)
|
)
|
||||||
# The context answers a handle from what was stated on it, not from
|
|
||||||
# this module global, so a rank stood up by hand says so itself.
|
|
||||||
get_parallel().override_permanently(world_group=ps._WORLD)
|
get_parallel().override_permanently(world_group=ps._WORLD)
|
||||||
atexit.register(dist.destroy_process_group)
|
atexit.register(dist.destroy_process_group)
|
||||||
logging.disable(logging.INFO)
|
logging.disable(logging.INFO)
|
||||||
|
|||||||
@@ -93,8 +93,6 @@ def _init_cpu_group_once() -> dist.ProcessGroup:
|
|||||||
local_rank=local_rank,
|
local_rank=local_rank,
|
||||||
backend="nccl",
|
backend="nccl",
|
||||||
)
|
)
|
||||||
# The context answers a handle from what was stated on it, not from
|
|
||||||
# this module global, so a rank stood up by hand says so itself.
|
|
||||||
get_parallel().override_permanently(world_group=coord)
|
get_parallel().override_permanently(world_group=coord)
|
||||||
atexit.register(dist.destroy_process_group)
|
atexit.register(dist.destroy_process_group)
|
||||||
cpu_group = coord.cpu_group
|
cpu_group = coord.cpu_group
|
||||||
|
|||||||
@@ -66,8 +66,6 @@ def _init_world():
|
|||||||
local_rank=local_rank,
|
local_rank=local_rank,
|
||||||
backend="nccl",
|
backend="nccl",
|
||||||
)
|
)
|
||||||
# The context answers a handle from what was stated on it, not from
|
|
||||||
# this module global, so a rank stood up by hand says so itself.
|
|
||||||
get_parallel().override_permanently(world_group=coord)
|
get_parallel().override_permanently(world_group=coord)
|
||||||
atexit.register(dist.destroy_process_group)
|
atexit.register(dist.destroy_process_group)
|
||||||
logging.disable(logging.INFO)
|
logging.disable(logging.INFO)
|
||||||
|
|||||||
@@ -54,8 +54,6 @@ def _init_world():
|
|||||||
local_rank=local_rank,
|
local_rank=local_rank,
|
||||||
backend="nccl",
|
backend="nccl",
|
||||||
)
|
)
|
||||||
# The context answers a handle from what was stated on it, not from
|
|
||||||
# this module global, so a rank stood up by hand says so itself.
|
|
||||||
get_parallel().override_permanently(world_group=coord)
|
get_parallel().override_permanently(world_group=coord)
|
||||||
atexit.register(dist.destroy_process_group)
|
atexit.register(dist.destroy_process_group)
|
||||||
cpu_group = coord.cpu_group
|
cpu_group = coord.cpu_group
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ register_cuda_ci(est_time=10, stage="base-b", runner_config="1-gpu-large")
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def stated_tp_group():
|
def stated_tp_group():
|
||||||
"""A TP group for a test that runs in a process without one.
|
"""Provide a TP-group placeholder for kernels with mocked symmetric memory."""
|
||||||
|
|
||||||
The production call passes the group *into* `use_symmetric_memory`, so
|
|
||||||
stubbing that context manager does not stop the read -- the argument is
|
|
||||||
evaluated first. Stating it on the context answers every spelling.
|
|
||||||
"""
|
|
||||||
from sglang.srt.runtime_context import get_parallel
|
from sglang.srt.runtime_context import get_parallel
|
||||||
|
|
||||||
with get_parallel().override(tp_group=None):
|
with get_parallel().override(tp_group=None):
|
||||||
@@ -35,10 +30,7 @@ def test_mhc_fused_post_pre_matches_unfused(
|
|||||||
pytest.skip("CUDA is required for TileLang mHC kernels")
|
pytest.skip("CUDA is required for TileLang mHC kernels")
|
||||||
|
|
||||||
monkeypatch.setattr(mhc, "is_dsa_prefill_cp_interleave", lambda: False)
|
monkeypatch.setattr(mhc, "is_dsa_prefill_cp_interleave", lambda: False)
|
||||||
# This is a single-process kernel unit test with no TP group initialized.
|
# Disable symmetric-memory allocation for this single-process kernel test.
|
||||||
# mhc_pre / mhc_fused_post_pre allocate the MoE input in the symmetric-memory
|
|
||||||
# pool, which asks for the TP group; bypassing the allocation is enough, and
|
|
||||||
# then nothing asks. Mirrors the workaround in test_mxfp4_sm90_cutlass.py.
|
|
||||||
monkeypatch.setattr(mhc, "use_symmetric_memory", lambda *a, **kw: nullcontext())
|
monkeypatch.setattr(mhc, "use_symmetric_memory", lambda *a, **kw: nullcontext())
|
||||||
monkeypatch.setattr(mhc, "is_allocation_symmetric", lambda: False)
|
monkeypatch.setattr(mhc, "is_allocation_symmetric", lambda: False)
|
||||||
torch.manual_seed(0)
|
torch.manual_seed(0)
|
||||||
|
|||||||
@@ -37,12 +37,7 @@ dev = "cuda"
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def stated_tp_group():
|
def stated_tp_group():
|
||||||
"""A TP group for a test that runs in a process without one.
|
"""Provide a TP-group placeholder for kernels with mocked symmetric memory."""
|
||||||
|
|
||||||
The production call passes the group *into* `use_symmetric_memory`, so
|
|
||||||
stubbing that context manager does not stop the read -- the argument is
|
|
||||||
evaluated first. Stating it on the context answers every spelling.
|
|
||||||
"""
|
|
||||||
from sglang.srt.runtime_context import get_parallel
|
from sglang.srt.runtime_context import get_parallel
|
||||||
|
|
||||||
with get_parallel().override(tp_group=None):
|
with get_parallel().override(tp_group=None):
|
||||||
|
|||||||
@@ -129,9 +129,6 @@ def mixer2_gated_norm_tensor_parallel(
|
|||||||
)
|
)
|
||||||
mixer.weight.weight_loader(mixer.weight, weight)
|
mixer.weight.weight_loader(mixer.weight, weight)
|
||||||
|
|
||||||
# m2 reads tp via get_parallel().tp_size/rank — state a single-rank topology
|
|
||||||
# through the context. Every width that follows from `tp_size` is named:
|
|
||||||
# narrowing one leaf and leaving the quotients behind describes no layout.
|
|
||||||
with get_parallel().override(
|
with get_parallel().override(
|
||||||
tp_size=1,
|
tp_size=1,
|
||||||
tp_rank=0,
|
tp_rank=0,
|
||||||
|
|||||||
@@ -37,10 +37,7 @@ def _mock_global_server_args(backend="pytorch"):
|
|||||||
class _DummyTPGroup:
|
class _DummyTPGroup:
|
||||||
device_group = None
|
device_group = None
|
||||||
|
|
||||||
# `Sampler.__init__` asks the context for the group; state one for the rest
|
# Provide a TP group for sampler initialization without distributed setup.
|
||||||
# of the process, since this process has no distributed init. Not the scoped
|
|
||||||
# `override()`: its context manager would be collected here and take the
|
|
||||||
# value back down with it.
|
|
||||||
get_parallel().override_permanently(tp_group=_DummyTPGroup())
|
get_parallel().override_permanently(tp_group=_DummyTPGroup())
|
||||||
from sglang.srt.runtime_context import get_flags
|
from sglang.srt.runtime_context import get_flags
|
||||||
|
|
||||||
|
|||||||
@@ -47,12 +47,9 @@ register_cpu_ci(est_time=5, suite="stage-b-test-cpu-intel")
|
|||||||
|
|
||||||
|
|
||||||
def _make_scheduler(grammar_backend_name="none", skip_tokenizer=False):
|
def _make_scheduler(grammar_backend_name="none", skip_tokenizer=False):
|
||||||
"""Create a mock scheduler with necessary attributes.
|
"""Create a mock scheduler and publish its configuration and placement.
|
||||||
|
|
||||||
The grammar manager reads its config and its place in the pipeline from
|
The caller must reset the context during teardown.
|
||||||
the context, so the settings that used to be hung off the mock are
|
|
||||||
published instead. The caller resets the context; every test here goes
|
|
||||||
through `_GrammarFixture`.
|
|
||||||
"""
|
"""
|
||||||
reset_context()
|
reset_context()
|
||||||
server_args = ServerArgs(
|
server_args = ServerArgs(
|
||||||
@@ -778,9 +775,7 @@ class TestGrammarManagerPPSync(unittest.TestCase):
|
|||||||
enter_override(
|
enter_override(
|
||||||
self, get_context().override_server_args(skip_tokenizer_init=True)
|
self, get_context().override_server_args(skip_tokenizer_init=True)
|
||||||
)
|
)
|
||||||
# After that override, not before: installing a server-args override
|
# Override ranks after the server-args override rebuilds the config bags.
|
||||||
# re-resolves the parallel bag from defaults, which puts `pp_size`
|
|
||||||
# back to 1 whatever was published.
|
|
||||||
enter_scope(self, get_parallel().override(pp_size=pp_size, pp_rank=pp_rank))
|
enter_scope(self, get_parallel().override(pp_size=pp_size, pp_rank=pp_rank))
|
||||||
scheduler.pp_group = pp_group
|
scheduler.pp_group = pp_group
|
||||||
mgr = GrammarManager(scheduler)
|
mgr = GrammarManager(scheduler)
|
||||||
|
|||||||
@@ -204,8 +204,6 @@ class TestRegisterToBootstrap(CustomTestCase):
|
|||||||
def test_rust_attention_dp_replicates_complete_topology_across_hosts(
|
def test_rust_attention_dp_replicates_complete_topology_across_hosts(
|
||||||
self, mock_put
|
self, mock_put
|
||||||
):
|
):
|
||||||
# The consumer reads the group through `get_parallel()`, so the
|
|
||||||
# stub is stated there rather than in the module the build writes.
|
|
||||||
mock_world_group = MagicMock()
|
mock_world_group = MagicMock()
|
||||||
success_resp = MagicMock()
|
success_resp = MagicMock()
|
||||||
success_resp.status_code = 200
|
success_resp.status_code = 200
|
||||||
|
|||||||
@@ -45,9 +45,6 @@ def test_dp_leaders_reuse_node_local_ports(
|
|||||||
server_args=SimpleNamespace(),
|
server_args=SimpleNamespace(),
|
||||||
model_config=SimpleNamespace(is_multimodal=False),
|
model_config=SimpleNamespace(is_multimodal=False),
|
||||||
)
|
)
|
||||||
# Where this rank sits, stated whole: the attention rank follows
|
|
||||||
# from the TP rank and the attention-TP width, and the identities
|
|
||||||
# refuse the combination if it describes no real layout.
|
|
||||||
with parallel.override(
|
with parallel.override(
|
||||||
tp_rank=tp_rank,
|
tp_rank=tp_rank,
|
||||||
attn_dp_rank=dp_rank,
|
attn_dp_rank=dp_rank,
|
||||||
|
|||||||
@@ -93,8 +93,6 @@ def _make_prefill_aware_swa_runner(
|
|||||||
page_size=1,
|
page_size=1,
|
||||||
attn_cp_size=1,
|
attn_cp_size=1,
|
||||||
tp_size=1,
|
tp_size=1,
|
||||||
# The backend still reads the runner's frozen record for these two;
|
|
||||||
# same single-rank placement, stated where it looks for it.
|
|
||||||
ps=SimpleNamespace(attn_cp_size=1, tp_size=1),
|
ps=SimpleNamespace(attn_cp_size=1, tp_size=1),
|
||||||
is_draft_worker=False,
|
is_draft_worker=False,
|
||||||
server_args=server_args,
|
server_args=server_args,
|
||||||
|
|||||||
@@ -33,12 +33,7 @@ GROUP_SIZE = 32 # MXFP4 block size
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def stated_tp_group():
|
def stated_tp_group():
|
||||||
"""A TP group for a test that runs in a process without one.
|
"""Provide a TP-group placeholder for kernels with mocked symmetric memory."""
|
||||||
|
|
||||||
The production call passes the group *into* `use_symmetric_memory`, so
|
|
||||||
stubbing that context manager does not stop the read -- the argument is
|
|
||||||
evaluated first. Stating it on the context answers every spelling.
|
|
||||||
"""
|
|
||||||
from sglang.srt.runtime_context import get_parallel
|
from sglang.srt.runtime_context import get_parallel
|
||||||
|
|
||||||
with get_parallel().override(tp_group=None):
|
with get_parallel().override(tp_group=None):
|
||||||
|
|||||||
@@ -19,12 +19,7 @@ register_cuda_ci(est_time=14, stage="base-b", runner_config="1-gpu-small")
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def stated_tp_group():
|
def stated_tp_group():
|
||||||
"""A TP group for a test that runs in a process without one.
|
"""Provide a TP-group placeholder for kernels with mocked symmetric memory."""
|
||||||
|
|
||||||
The production call passes the group *into* `use_symmetric_memory`, so
|
|
||||||
stubbing that context manager does not stop the read -- the argument is
|
|
||||||
evaluated first. Stating it on the context answers every spelling.
|
|
||||||
"""
|
|
||||||
from sglang.srt.runtime_context import get_parallel
|
from sglang.srt.runtime_context import get_parallel
|
||||||
|
|
||||||
with get_parallel().override(tp_group=None):
|
with get_parallel().override(tp_group=None):
|
||||||
|
|||||||
@@ -65,12 +65,7 @@ GROUP_SIZE = 32 # MXFP4 block size
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def stated_tp_group():
|
def stated_tp_group():
|
||||||
"""A TP group for a test that runs in a process without one.
|
"""Provide a TP-group placeholder for kernels with mocked symmetric memory."""
|
||||||
|
|
||||||
The production call passes the group *into* `use_symmetric_memory`, so
|
|
||||||
stubbing that context manager does not stop the read -- the argument is
|
|
||||||
evaluated first. Stating it on the context answers every spelling.
|
|
||||||
"""
|
|
||||||
from sglang.srt.runtime_context import get_parallel
|
from sglang.srt.runtime_context import get_parallel
|
||||||
|
|
||||||
with get_parallel().override(tp_group=None):
|
with get_parallel().override(tp_group=None):
|
||||||
|
|||||||
@@ -64,8 +64,6 @@ def load_mlx_scheduler_module():
|
|||||||
class TestSchedulerIdleStepCounters(CustomTestCase):
|
class TestSchedulerIdleStepCounters(CustomTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
# The loop asks the context where this process sits; nothing here
|
|
||||||
# builds a process group, so the placement arrives by publishing one.
|
|
||||||
enter_scope(self, published_topology(role="scheduler"))
|
enter_scope(self, published_topology(role="scheduler"))
|
||||||
|
|
||||||
@parameterized.expand(
|
@parameterized.expand(
|
||||||
@@ -184,11 +182,7 @@ class TestSchedulerIdleStepCounters(CustomTestCase):
|
|||||||
f"{PDMUX_MODULE}.torch.cuda.stream",
|
f"{PDMUX_MODULE}.torch.cuda.stream",
|
||||||
side_effect=lambda stream: nullcontext(),
|
side_effect=lambda stream: nullcontext(),
|
||||||
),
|
),
|
||||||
# The prefill section runs under the duplicate communicator
|
# PD multiplexing requires a separate prefill communicator.
|
||||||
# `--enable-pdmux` builds, in place of the module flag this
|
|
||||||
# replaces. The loop has no process groups at all, so stand
|
|
||||||
# one in: the scope refuses to open without it rather than
|
|
||||||
# letting prefill quietly share the decode communicator.
|
|
||||||
patch.object(
|
patch.object(
|
||||||
parallel_state,
|
parallel_state,
|
||||||
"_PDMUX_PREFILL_TP_GROUP",
|
"_PDMUX_PREFILL_TP_GROUP",
|
||||||
|
|||||||
@@ -97,12 +97,10 @@ class TestLoadPublisherGating(CustomTestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def _build(self, *, config=ZMQ_ENDPOINT, explicit="auto", ranks=None, **topology):
|
def _build(self, *, config=ZMQ_ENDPOINT, explicit="auto", ranks=None, **topology):
|
||||||
"""Construct a publisher with the socket bind stubbed out, returning
|
"""Return a publisher and its mocked socket factory under a published topology.
|
||||||
(publisher, captured _open_pub_socket mock). Opts in via explicit="auto"
|
|
||||||
by default (the feature is off without it). The topology is published
|
``explicit="auto"`` enables load publication by default.
|
||||||
rather than overridden, so the ranks the publisher reads are the ones a
|
"""
|
||||||
layout of that shape actually produces; every read happens in the
|
|
||||||
constructor."""
|
|
||||||
with (
|
with (
|
||||||
published_topology(ranks=ranks, **topology),
|
published_topology(ranks=ranks, **topology),
|
||||||
patch(
|
patch(
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ class TestOutputStreamerCustomizedInfo(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
serving_patch.start()
|
serving_patch.start()
|
||||||
observability_patch.start()
|
observability_patch.start()
|
||||||
# The streamer asks the context which rank it is streaming from.
|
|
||||||
enter_scope(self, published_topology(ranks={"dp_rank": 0}))
|
enter_scope(self, published_topology(ranks={"dp_rank": 0}))
|
||||||
self.addCleanup(serving_patch.stop)
|
self.addCleanup(serving_patch.stop)
|
||||||
self.addCleanup(observability_patch.stop)
|
self.addCleanup(observability_patch.stop)
|
||||||
|
|||||||
@@ -23,11 +23,9 @@ register_cpu_ci(est_time=11, suite="base-a-test-cpu")
|
|||||||
|
|
||||||
|
|
||||||
def _published_topology():
|
def _published_topology():
|
||||||
"""The topology these tests run in.
|
"""Publish WORLD rank 12 with TP=8 and PP=2.
|
||||||
|
|
||||||
World rank 12 of a `tp=8, pp=2` world is `tp_rank=4` on the second stage,
|
This gives TP rank 4, PP rank 1, attention-DP rank 1, and attention-TP rank 0.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
return published_topology(
|
return published_topology(
|
||||||
role="scheduler",
|
role="scheduler",
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ def _make_scheduler(pending_req, *, chunked_req, running_reqs) -> Scheduler:
|
|||||||
|
|
||||||
class TestPendingChunkedAbortRace(CustomTestCase):
|
class TestPendingChunkedAbortRace(CustomTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# The abort path asks the context for the pipeline width.
|
|
||||||
enter_scope(self, published_topology())
|
enter_scope(self, published_topology())
|
||||||
|
|
||||||
def test_req_left_chunked_slot_is_aborted(self):
|
def test_req_left_chunked_slot_is_aborted(self):
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ class TestWaitingTimeout(CustomTestCase):
|
|||||||
|
|
||||||
class TestRunningTimeout(CustomTestCase):
|
class TestRunningTimeout(CustomTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# The poll asks the context for the pipeline width.
|
|
||||||
enter_scope(self, published_topology())
|
enter_scope(self, published_topology())
|
||||||
|
|
||||||
def test_emits_only_stale_unfinished_reqs_without_marking(self):
|
def test_emits_only_stale_unfinished_reqs_without_marking(self):
|
||||||
|
|||||||
@@ -297,7 +297,6 @@ class TestPPMambaPoolSizing(unittest.TestCase):
|
|||||||
server_args=SimpleNamespace(),
|
server_args=SimpleNamespace(),
|
||||||
spec_algorithm=SimpleNamespace(is_none=lambda: True),
|
spec_algorithm=SimpleNamespace(is_none=lambda: True),
|
||||||
layer_info=SimpleNamespace(start_layer=start, end_layer=end),
|
layer_info=SimpleNamespace(start_layer=start, end_layer=end),
|
||||||
# The runner carries its placement as plain attributes.
|
|
||||||
attn_dp_size=1,
|
attn_dp_size=1,
|
||||||
pp_size=pp_size,
|
pp_size=pp_size,
|
||||||
hybrid_gdn_config=None,
|
hybrid_gdn_config=None,
|
||||||
|
|||||||
@@ -302,10 +302,6 @@ class TestHostMemoryBudget(CustomTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_ranks_per_host_divides_world_size_by_nodes(self):
|
def test_ranks_per_host_divides_world_size_by_nodes(self):
|
||||||
# The launcher slices ranks uniformly across nodes, so the co-located
|
|
||||||
# rank count is world_size // nnodes — no hostname collective.
|
|
||||||
# tp_size=16 states the launch width the count divides -- the
|
|
||||||
# published configuration is where ranks_per_host reads it from.
|
|
||||||
with (
|
with (
|
||||||
get_context().override_server_args(nnodes=2, tp_size=16),
|
get_context().override_server_args(nnodes=2, tp_size=16),
|
||||||
unittest.mock.patch.object(
|
unittest.mock.patch.object(
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ def mock_cpu_env(kv_size=2, tp_size=1, swa_eviction_interval=4):
|
|||||||
|
|
||||||
with (
|
with (
|
||||||
patch("torch._utils._element_size", return_value=kv_size),
|
patch("torch._utils._element_size", return_value=kv_size),
|
||||||
# The whole attention triple, not just one leaf: a width that does not
|
|
||||||
# factor describes no layout.
|
|
||||||
get_parallel().override(
|
get_parallel().override(
|
||||||
tp_size=tp_size,
|
tp_size=tp_size,
|
||||||
attn_tp_size=tp_size,
|
attn_tp_size=tp_size,
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ class TestTransformersFallbackSkipSubstrs(CustomTestCase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
with (
|
with (
|
||||||
# `__init__` only stashes the pipeline group, so an empty
|
|
||||||
# stand-in carries it past the read.
|
|
||||||
get_parallel().override(pp_group=SimpleNamespace()),
|
get_parallel().override(pp_group=SimpleNamespace()),
|
||||||
patch(
|
patch(
|
||||||
"sglang.srt.models.transformers.get_hf_text_config",
|
"sglang.srt.models.transformers.get_hf_text_config",
|
||||||
|
|||||||
@@ -83,9 +83,6 @@ class TestGlm5NextBfgFusion(unittest.TestCase):
|
|||||||
for attn_tp, rank in ((1, 0), (2, 0), (2, 1)):
|
for attn_tp, rank in ((1, 0), (2, 0), (2, 1)):
|
||||||
with (
|
with (
|
||||||
self.subTest(route=expected_route, attn_tp=attn_tp, rank=rank),
|
self.subTest(route=expected_route, attn_tp=attn_tp, rank=rank),
|
||||||
# A width is a whole topology: the attention triple has
|
|
||||||
# to factor `tp_size`, and this process has to sit where
|
|
||||||
# the triple puts it.
|
|
||||||
get_parallel().override(
|
get_parallel().override(
|
||||||
tp_size=4,
|
tp_size=4,
|
||||||
tp_rank=rank,
|
tp_rank=rank,
|
||||||
|
|||||||
@@ -64,12 +64,7 @@ class _DummyPublisherThread:
|
|||||||
|
|
||||||
|
|
||||||
def _publish_server_args(test, **fields):
|
def _publish_server_args(test, **fields):
|
||||||
"""Publish a config for the reporter under test and return the instance.
|
"""Install reporter configuration and rank overrides, with test cleanup."""
|
||||||
|
|
||||||
The collector asks the context where this process sits, so the ranks are
|
|
||||||
stated too: without them a rank read falls through to a process group that
|
|
||||||
a unit test has not built.
|
|
||||||
"""
|
|
||||||
fields.setdefault("decode_log_interval", 40)
|
fields.setdefault("decode_log_interval", 40)
|
||||||
override = get_context().override_server_args(**fields)
|
override = get_context().override_server_args(**fields)
|
||||||
server_args = override.install()
|
server_args = override.install()
|
||||||
@@ -297,8 +292,6 @@ class TestForwardPassMetrics(unittest.TestCase):
|
|||||||
forward_pass_metrics_ipc_name=None,
|
forward_pass_metrics_ipc_name=None,
|
||||||
kv_events_config=None,
|
kv_events_config=None,
|
||||||
)
|
)
|
||||||
# The reporter asks the context whether this is the last stage, and
|
|
||||||
# which replica it is reporting for.
|
|
||||||
enter_scope(self, get_parallel().override(pp_rank=0, pp_size=1, dp_rank=2))
|
enter_scope(self, get_parallel().override(pp_rank=0, pp_size=1, dp_rank=2))
|
||||||
scheduler.enable_kv_cache_events = False
|
scheduler.enable_kv_cache_events = False
|
||||||
|
|
||||||
@@ -336,7 +329,6 @@ class TestForwardPassMetrics(unittest.TestCase):
|
|||||||
forward_pass_metrics_ipc_name=None,
|
forward_pass_metrics_ipc_name=None,
|
||||||
kv_events_config=None,
|
kv_events_config=None,
|
||||||
)
|
)
|
||||||
# The reporter asks the context whether this is the last stage.
|
|
||||||
enter_scope(self, get_parallel().override(pp_rank=0, pp_size=2))
|
enter_scope(self, get_parallel().override(pp_rank=0, pp_size=2))
|
||||||
scheduler.enable_kv_cache_events = False
|
scheduler.enable_kv_cache_events = False
|
||||||
|
|
||||||
|
|||||||
@@ -132,15 +132,7 @@ def _stash_overlay(server_args):
|
|||||||
|
|
||||||
|
|
||||||
def _live_topology_leaves():
|
def _live_topology_leaves():
|
||||||
"""Names `ParallelContext` answers from a runtime write, not the config.
|
"""Return runtime-only parallel fields, identified by declarations without ``fn``."""
|
||||||
|
|
||||||
Read off the declarations that carry no `fn`, which is what those are.
|
|
||||||
Inferring them from "did the read raise" is wrong -- it raises only while
|
|
||||||
nothing has written the name, so in a process where an earlier test stated
|
|
||||||
one the property answers that value and a leaf check reads it as a config
|
|
||||||
mismatch (`parallel.tp_size: bag=1 resolution=2`). Whether a name is
|
|
||||||
shadowed is a property of the declaration, not of the process.
|
|
||||||
"""
|
|
||||||
from sglang.srt.runtime_context import _derived_widths
|
from sglang.srt.runtime_context import _derived_widths
|
||||||
|
|
||||||
return frozenset(n for n, d in _derived_widths().items() if not d.fn)
|
return frozenset(n for n, d in _derived_widths().items() if not d.fn)
|
||||||
|
|||||||
@@ -249,7 +249,6 @@ def test_worker_folds_a_gate_admitted_quantized_selector_head(monkeypatch):
|
|||||||
block_size=8,
|
block_size=8,
|
||||||
selector=object(),
|
selector=object(),
|
||||||
model_runner=SimpleNamespace(tp_rank=0),
|
model_runner=SimpleNamespace(tp_rank=0),
|
||||||
# The worker rank-gates its logging on its own frozen record.
|
|
||||||
ps=SimpleNamespace(tp_rank=0),
|
ps=SimpleNamespace(tp_rank=0),
|
||||||
draft_model=SimpleNamespace(lm_head=None),
|
draft_model=SimpleNamespace(lm_head=None),
|
||||||
device="cpu",
|
device="cpu",
|
||||||
@@ -285,7 +284,6 @@ def test_worker_warns_once_when_selector_sampling_is_disabled(monkeypatch):
|
|||||||
_selector_sampling_enabled=False,
|
_selector_sampling_enabled=False,
|
||||||
_warned_sampling_fallback=False,
|
_warned_sampling_fallback=False,
|
||||||
model_runner=SimpleNamespace(tp_rank=0),
|
model_runner=SimpleNamespace(tp_rank=0),
|
||||||
# The worker rank-gates its logging on its own frozen record.
|
|
||||||
ps=SimpleNamespace(tp_rank=0),
|
ps=SimpleNamespace(tp_rank=0),
|
||||||
)
|
)
|
||||||
batch = SimpleNamespace(sampling_info=SimpleNamespace(is_all_greedy=False))
|
batch = SimpleNamespace(sampling_info=SimpleNamespace(is_all_greedy=False))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -316,7 +316,6 @@ class _SchedulerStub:
|
|||||||
|
|
||||||
class TestSchedulerRecordWeightVersionChange(CustomTestCase):
|
class TestSchedulerRecordWeightVersionChange(CustomTestCase):
|
||||||
def _scheduler(self, *args, pp_size=1, **kwargs):
|
def _scheduler(self, *args, pp_size=1, **kwargs):
|
||||||
# The recording path asks the context for the pipeline width.
|
|
||||||
enter_scope(self, published_topology(pp_size=pp_size))
|
enter_scope(self, published_topology(pp_size=pp_size))
|
||||||
scheduler = _SchedulerStub(*args, **kwargs)
|
scheduler = _SchedulerStub(*args, **kwargs)
|
||||||
for name, value in (
|
for name, value in (
|
||||||
|
|||||||
Reference in New Issue
Block a user