Name the two widths of the WORLD group (#40070)

This commit is contained in:
Cheng Wan
2026-09-18 17:47:02 -07:00
committed by GitHub
parent 81421b91e9
commit fa7e83fd09
13 changed files with 178 additions and 45 deletions
@@ -65,7 +65,8 @@ class AscendTransferEngine(MooncakeTransferEngine):
# through all_gather to avoid conflicts with rdma initialization.
tmp_tensor = torch.zeros(1, device="npu")
output_tensor_list = [
torch.empty_like(tmp_tensor) for _ in range(get_parallel().world_size)
torch.empty_like(tmp_tensor)
for _ in range(get_parallel().launch_world_size)
]
torch.distributed.all_gather(
output_tensor_list,
+1 -1
View File
@@ -92,7 +92,7 @@ class ElasticEPStateManager:
if get_exec().moe.elastic_ep_backend is not None:
world_size = torch.distributed.get_world_size()
active_rank_capacity = get_parallel().max_ep_size or world_size
active_rank_capacity = get_parallel().max_world_size
assert active_rank_capacity >= world_size, (
f"--max-ep-size ({active_rank_capacity}) must be >= "
f"world_size ({world_size})."
@@ -50,7 +50,7 @@ class ExpertBackupClient:
self.buffer_size = 0
self.use_backup = False
local_ip = get_local_ip_auto()
all_ips = [None] * get_parallel().world_size
all_ips = [None] * get_parallel().launch_world_size
torch.distributed.all_gather_object(
all_ips, local_ip, group=get_parallel().world_group.cpu_group
)
@@ -59,14 +59,14 @@ class ExpertBackupClient:
for i in range(self.engine_num):
self.recv_list[i] = context.socket(zmq.SUB)
self.recv_list[i].connect(
f"tcp://{all_ips[i * get_parallel().world_size // get_parallel().nnodes]}:{PORT_BASE + i * 2 + 1}"
f"tcp://{all_ips[i * get_parallel().launch_world_size // get_parallel().nnodes]}:{PORT_BASE + i * 2 + 1}"
)
self.recv_list[i].setsockopt(zmq.SUBSCRIBE, b"")
# Synchronization channel to notify the manager when this client is ready.
self.ready_sockets[i] = context.socket(zmq.PUSH)
self.ready_sockets[i].connect(
f"tcp://{all_ips[i * get_parallel().world_size // get_parallel().nnodes]}:{PORT_BASE + i * 2}"
f"tcp://{all_ips[i * get_parallel().launch_world_size // get_parallel().nnodes]}:{PORT_BASE + i * 2}"
)
sock_send(self.ready_sockets[i], UpdateExpertBackupReq())
+1 -1
View File
@@ -5665,7 +5665,7 @@ class Scheduler(
old_ep_size = ElasticEPStateManager.get_effective_ep_size()
new_ep_size = recv_req.new_ep_size
max_ep_size = get_parallel().max_ep_size or old_ep_size
max_ep_size = get_parallel().max_world_size
logger.debug(
"[Elastic EP][scale] request received: new_ep_size=%d "
@@ -60,13 +60,17 @@ def _resolve_elastic_world_dp_size(
live_dp_size = get_parallel().attn_dp_size
effective_ep_size = ElasticEPStateManager.get_effective_ep_size()
# The group's own membership, not the width it was built at: this is the
# 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)
if live_dp_size != effective_ep_size:
raise RuntimeError(
"[Elastic EP] WORLD MLP sync dp_size is out of sync: "
f"rank={torch.distributed.get_rank(group)} "
f"live_dp_size={live_dp_size} effective_ep_size={effective_ep_size} "
f"live_dp_size={live_dp_size} "
f"effective_ep_size={effective_ep_size} "
f"world_size={world_size} server_args_dp_size={dp_size} "
f"local_num_tokens={local_num_tokens} "
f"local_forward_mode={local_forward_mode}"
@@ -2195,7 +2195,7 @@ class KVCacheConfigurator:
available_gpu_memory = get_available_gpu_memory(
self.device,
self.gpu_id,
distributed=get_parallel().world_group.world_size > 1,
distributed=get_parallel().launch_world_size > 1,
cpu_group=get_parallel().world_group.cpu_group,
)
@@ -31,20 +31,21 @@ _WRITE_BACK_STAGING_PAGE_CHUNK = 64
def ranks_per_host() -> int:
"""Number of ranks of this job running on the same machine as this one.
Derived as world_size // nnodes: the launcher slices ranks 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.
Derived as the launch width // nnodes: the launcher slices ranks
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()):
return 1
try:
world_group = get_parallel().world_group
launch_world_size = get_parallel().launch_world_size
except AssertionError:
return 1
if world_group.world_size == 1:
if launch_world_size == 1:
return 1
return max(world_group.world_size // get_parallel().nnodes, 1)
return max(launch_world_size // get_parallel().nnodes, 1)
def host_memory_budget_bytes() -> int:
@@ -146,7 +146,7 @@ def build_attention_backends(*, model_runner: ModelRunner) -> AttentionBackends:
model_runner.device,
model_runner.gpu_id,
get_parallel().world_group.rank_in_group,
get_parallel().world_group.world_size,
get_parallel().launch_world_size,
get_parallel().world_group.cpu_group,
)
@@ -227,7 +227,7 @@ def refresh_deep_gemm_layout_memory_budget(
available_memory_gb = get_available_gpu_memory(
model_runner.device,
model_runner.gpu_id,
distributed=world_group.world_size > 1,
distributed=get_parallel().launch_world_size > 1,
cpu_group=world_group.cpu_group,
)
budget_bytes = set_masked_standard_layout_memory_budget(
@@ -59,7 +59,7 @@ def compute_post_capture_kv_resize(
free_gb = get_available_gpu_memory(
model_runner.device,
model_runner.gpu_id,
distributed=get_parallel().world_group.world_size > 1,
distributed=get_parallel().launch_world_size > 1,
cpu_group=get_parallel().world_group.cpu_group,
)
headroom_gb = model_runner.pre_model_load_memory * (
@@ -1008,7 +1008,7 @@ def _prefetch_all_checkpoints(
if torch.distributed.is_initialized():
world_group = get_parallel().world_group
local_rank = world_group.local_rank
local_world_size = world_group.local_size or world_group.world_size
local_world_size = world_group.local_size or get_parallel().launch_world_size
else:
local_rank = 0
local_world_size = 1
+72 -10
View File
@@ -117,9 +117,57 @@ def _parallel_config_leaves() -> frozenset:
# this process's attention-DP rank.
_MISSING_READ = object()
class Live(msgspec.Struct, frozen=True):
"""How a rank / group / world width is answered, and what it means.
`source` is the canonical getter's name in `parallel_state`, a callable
taking the context, or `None` for a name only a stamp can answer.
Most entries in the table below are a bare getter name: a rank or a group
handle is its own explanation. This shape is for a name whose meaning is
not in its getter, and it carries the prose with the declaration rather
than in a second table keyed by the same names.
"""
source: Any = None
doc: str = ""
_LIVE_READS: dict = {
"world_size": "get_world_size",
"world_rank": "get_world_rank",
# Two widths of the WORLD group: what it was built at, and what it has
# room for. Both are properties of the group itself. How much of that room
# is currently serving is elastic-EP state, owned by `ElasticEPStateManager`
# and asked of it directly -- a width that lives somewhere else does not
# become a WORLD fact by being readable from here.
"launch_world_size": Live(
source="get_world_size",
doc=(
"Width the WORLD group was built at: `len(ranks)`, frozen when the "
"coordinator was constructed. What every startup reader wants -- "
"memory accounting, KV cache sizing, graph capture, weight loading "
"-- and what a scale-up leaves behind rather than updates."
),
),
"max_world_size": Live(
source=lambda self: self.max_ep_size or self.launch_world_size,
doc=(
"Ranks the WORLD group has room for: `--max-ep-size` when it is "
"set, otherwise the launch width. This is the ceiling the process "
"group was pre-allocated to -- mooncake sizes its active-rank mask "
"to it -- which is why `init_distributed_environment` takes it "
"under this name. Whether the group can grow at all is a separate "
"question, answered by the leaf being set rather than by this width."
),
),
"launch_world_rank": Live(
source="get_world_rank",
doc=(
"This process's rank in the WORLD group as built. Frozen with the "
"coordinator, exactly like `launch_world_size`, and named for the "
"same reason: a scale-up does not renumber it."
),
),
"tp_rank": "get_tensor_model_parallel_rank",
"pp_rank": "get_pipeline_model_parallel_rank",
"moe_ep_rank": "get_moe_expert_parallel_rank",
@@ -195,9 +243,15 @@ def derive_parallel_widths(
the arithmetic lives here rather than being read back off the group
coordinators.
`world_size` is not among them: it is not a quotient, and `get_world_size()`
answers with the live WORLD group, which stays right through an elastic
scale-up that a value fixed at group build would not survive.
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 {
"attn_dp_size": attn_dp_size,
@@ -335,10 +389,11 @@ class ParallelContext:
return getattr(config, name)
live = _LIVE_READS.get(name, _MISSING_READ)
if live is not _MISSING_READ:
if isinstance(live, str):
return getattr(_ps(), live)()
if live is not None:
return live(self)
source = live.source if isinstance(live, Live) else live
if isinstance(source, str):
return getattr(_ps(), source)()
if source is not None:
return source(self)
raise RuntimeError(
f"parallel rank {name!r} is not available: it is computed from "
"this process's `tp_rank` when the attention topology is "
@@ -422,8 +477,15 @@ def _install_parallel_properties() -> None:
chain rather than one per kind of name.
"""
docs = {name: decl.doc for name, decl in _derived_widths().items()}
docs.update(
{
name: live.doc
for name, live in _LIVE_READS.items()
if isinstance(live, Live) and live.doc
}
)
for name in list(docs) + list(_LIVE_READS):
for name in list(_derived_widths()) + list(_LIVE_READS):
def getter(self, _name=name):
return self._read(_name)