Signed-off-by: Kushan A. <kahmadian@nvidia.com> Signed-off-by: kushanam <kushanam@users.noreply.github.com> Signed-off-by: Kushan A <kahmadian@nvidia.com> Signed-off-by: kahmadian <kahmadian@users.noreply.github.com> Co-authored-by: kushanam <kushanam@users.noreply.github.com> Co-authored-by: kahmadian <kahmadian@users.noreply.github.com>
379 lines
17 KiB
Python
379 lines
17 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""Translate a ``StrategySpec`` stack into vLLM-Omni parallel sizing.
|
|
|
|
Read a stack of strategy specs (one per mesh axis) and work out the
|
|
tensor/data/pipeline parallel sizes, the dense-EP flag, and the number of stage
|
|
replicas, plus who owns each axis's request routing. The result is a plain,
|
|
CPU-computable sizing struct (:class:`OmniParallelConfig`) keyed by the real
|
|
``OmniEngineArgs`` / ``EngineArgs`` field names, so the deploy layer can splat it
|
|
onto a stage's engine args.
|
|
|
|
The distinction this module enforces — engine data parallelism (a true vLLM
|
|
intra-engine world dimension) vs. omni stage replicas (independent engines fanned
|
|
out by omni's coordinator) — is documented on the axis validators that depend on
|
|
it (see :func:`_validate_dp` and :func:`_stage_replica_lb_policy`). Routing we do
|
|
not support yet (key-stable / affinity routing) raises ``NotImplementedError``;
|
|
any other invalid spec raises :class:`AxisTranslationError`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping, Sequence
|
|
from dataclasses import dataclass, field
|
|
from typing import Literal, NoReturn, cast, get_args
|
|
|
|
from vllm.logger import init_logger
|
|
|
|
from vllm_omni.config.composable_parallel.routing import (
|
|
Broadcast,
|
|
PartitionByHash,
|
|
PipelineMicrobatch,
|
|
RouteByStage,
|
|
RoutingPattern,
|
|
)
|
|
from vllm_omni.config.composable_parallel.spec import MeshAxisKind, StrategySpec
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
# Who owns an axis's request routing. A closed string type so an unexpected raw
|
|
# value is caught statically and at runtime (``_VALID_L1_OWNERS`` is derived from
|
|
# it, keeping one source of truth) rather than silently flowing through.
|
|
L1Owner = Literal["delegated", "engine"]
|
|
|
|
# DP, TP, PP, (dense) EP and stage_replica translate today. DP/TP/PP are true
|
|
# world dimensions vLLM realizes intra-engine; dense EP is a flag over the
|
|
# existing TP*DP ranks; stage_replica is an omni-coordinator-level fan-out of
|
|
# independent engine replicas (NOT a vLLM world dimension). The remaining kinds
|
|
# (sequence/context parallel, CFG, VAE pipelines, ...) need per-layer hooks or
|
|
# custom collectives and arrive in later stages.
|
|
_SUPPORTED_KINDS: tuple[MeshAxisKind, ...] = ("dp", "tp", "pp", "ep", "stage_replica")
|
|
|
|
# Which EngineArgs field each axis kind *sizes*. EP and stage_replica are
|
|
# deliberately absent: EP is not an independent world dimension but an
|
|
# ``enable_expert_parallel`` flag whose degree equals tp*dp; stage_replica is a
|
|
# per-stage deploy ``num_replicas`` count, not an intra-engine world dimension.
|
|
_AXIS_TO_ENGINE_FIELD: dict[MeshAxisKind, str] = {
|
|
"tp": "tensor_parallel_size",
|
|
"dp": "data_parallel_size",
|
|
"pp": "pipeline_parallel_size",
|
|
}
|
|
|
|
# Default L1 owner per axis kind. ``stage_replica`` is the only delegated axis:
|
|
# omni's coordinator/StagePool owns routing across replicas. ``dp`` is engine
|
|
# data parallelism, realized by vLLM's own intra-engine DP load balancer.
|
|
_DEFAULT_L1_OWNER: dict[MeshAxisKind, L1Owner] = {
|
|
"dp": "engine",
|
|
"tp": "engine",
|
|
"pp": "engine",
|
|
"ep": "engine",
|
|
"stage_replica": "delegated",
|
|
}
|
|
|
|
# How a RouteByStage policy maps onto omni's load balancer policy string. These
|
|
# are the stateless options omni can actually do; note there's no "hash" here,
|
|
# because omni has no key-stable balancer.
|
|
_STAGE_POLICY_TO_OMNI_LB: dict[str, str] = {
|
|
"random": "random",
|
|
"round_robin": "round-robin",
|
|
"least_queue": "least-queue-length",
|
|
}
|
|
|
|
_VALID_L1_OWNERS: frozenset[str] = frozenset(get_args(L1Owner))
|
|
|
|
|
|
class AxisTranslationError(ValueError):
|
|
"""Error for an invalid or unsupported strategy spec.
|
|
|
|
A single error type (rather than a tree of subclasses) keeps the public
|
|
surface small and consistent with the rest of the codebase; the specific
|
|
cause is in the message and is logged before the raise so it is visible even
|
|
when the type is unavailable to a caller (e.g. across a server boundary).
|
|
|
|
Strategies that are *valid but not built yet* — key-stable / affinity routing
|
|
today — raise ``NotImplementedError`` instead, to distinguish "we haven't
|
|
implemented this" from "your config is wrong".
|
|
"""
|
|
|
|
|
|
def _fail(msg: str) -> NoReturn:
|
|
"""Log and raise :class:`AxisTranslationError` for an invalid/unsupported spec."""
|
|
logger.error("[composable_parallel] %s", msg)
|
|
raise AxisTranslationError(msg)
|
|
|
|
|
|
def _not_implemented(msg: str) -> NoReturn:
|
|
"""Log and raise ``NotImplementedError`` for a valid-but-unbuilt strategy."""
|
|
logger.error("[composable_parallel] %s", msg)
|
|
raise NotImplementedError(msg)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class OmniParallelConfig:
|
|
"""Result of translating a spec stack into omni parallel sizing."""
|
|
|
|
tensor_parallel_size: int = 1
|
|
data_parallel_size: int = 1
|
|
pipeline_parallel_size: int = 1
|
|
# Dense expert parallelism: a flag over the TP*DP ranks, not a world axis.
|
|
enable_expert_parallel: bool = False
|
|
# Number of independent omni stage replicas (the ``stage_replica`` axis).
|
|
# This is NOT a vLLM world dimension; it is the per-stage deploy
|
|
# ``num_replicas`` count. 1 means a single (un-replicated) engine.
|
|
stage_replica_size: int = 1
|
|
# The omni StagePool LB policy string for the (delegated) stage_replica
|
|
# axis, if any. Only meaningful when ``stage_replica_size > 1``.
|
|
omni_lb_policy: str | None = None
|
|
# axis kind -> resolved L1 owner ("delegated" | "engine").
|
|
l1_owners: Mapping[MeshAxisKind, L1Owner] = field(default_factory=dict)
|
|
|
|
@property
|
|
def world_size(self) -> int:
|
|
# EP and stage_replica are intentionally excluded. EP reuses the TP*DP
|
|
# ranks rather than adding a dimension; stage_replica spins up separate
|
|
# engines (each its own world), not extra ranks in this engine's group.
|
|
# This product is what vLLM calls ``world_size_across_dp`` (tp * pp * dp).
|
|
return self.tensor_parallel_size * self.data_parallel_size * self.pipeline_parallel_size
|
|
|
|
@property
|
|
def delegated_axes(self) -> tuple[MeshAxisKind, ...]:
|
|
return tuple(kind for kind, owner in self.l1_owners.items() if owner == "delegated")
|
|
|
|
def as_engine_kwargs(self) -> dict[str, object]:
|
|
"""Return per-stage kwargs keyed by real OmniEngineArgs/EngineArgs field names.
|
|
|
|
Two derived values are intentionally *not* emitted here because they are
|
|
not per-stage engine args:
|
|
|
|
* ``stage_replica_size`` — a per-stage deploy ``num_replicas`` knob
|
|
(``StageDeployConfig``); the deploy layer consumes it separately.
|
|
* ``omni_lb_policy`` — a pipeline-wide load-balancer policy the engine
|
|
reads once at construction (see ``StrategyApplyResult.omni_lb_policy``);
|
|
it is applied at the orchestrator level, not folded into per-stage args.
|
|
"""
|
|
kwargs: dict[str, object] = {
|
|
"tensor_parallel_size": self.tensor_parallel_size,
|
|
"data_parallel_size": self.data_parallel_size,
|
|
"pipeline_parallel_size": self.pipeline_parallel_size,
|
|
}
|
|
if self.enable_expert_parallel:
|
|
kwargs["enable_expert_parallel"] = True
|
|
return kwargs
|
|
|
|
|
|
def _is_affinity_dp_routing(routing: RoutingPattern) -> bool:
|
|
"""True when DP routing demands key-stable (hash) placement."""
|
|
if isinstance(routing, PartitionByHash):
|
|
return True
|
|
if isinstance(routing, RouteByStage) and routing.routing_policy == "hash":
|
|
return True
|
|
return False
|
|
|
|
|
|
def _resolve_l1_owner(spec: StrategySpec) -> L1Owner:
|
|
kind = spec.mesh_axis.kind
|
|
declared_owner = spec.shard_extension.get("l1_owner")
|
|
owner = declared_owner
|
|
if owner is None:
|
|
owner = _DEFAULT_L1_OWNER.get(kind, "engine")
|
|
if kind not in _DEFAULT_L1_OWNER:
|
|
logger.debug(
|
|
"[composable_parallel] axis %r has unknown kind %r with no default l1_owner; falling back to %r",
|
|
spec.name,
|
|
kind,
|
|
owner,
|
|
)
|
|
else:
|
|
logger.debug(
|
|
"[composable_parallel] axis %r (kind %r) declared no l1_owner; using default %r",
|
|
spec.name,
|
|
kind,
|
|
owner,
|
|
)
|
|
if owner not in _VALID_L1_OWNERS:
|
|
# Distinguish a bad value supplied via shard_extension from a bad default
|
|
# indexed out of _DEFAULT_L1_OWNER, so the source of the invalid owner is
|
|
# debuggable rather than swallowed.
|
|
source = "shard_extension" if declared_owner is not None else "_DEFAULT_L1_OWNER"
|
|
logger.debug(
|
|
"[composable_parallel] axis %r (kind %r) resolved invalid l1_owner %r from %s; valid owners are %s",
|
|
spec.name,
|
|
kind,
|
|
owner,
|
|
source,
|
|
sorted(_VALID_L1_OWNERS),
|
|
)
|
|
_fail(f"axis {kind!r} has invalid l1_owner {owner!r}; expected one of {sorted(_VALID_L1_OWNERS)}")
|
|
return cast(L1Owner, owner)
|
|
|
|
|
|
def _validate_dp(spec: StrategySpec, owner: L1Owner) -> None:
|
|
"""Validate an engine data-parallel axis.
|
|
|
|
DP is realized intra-engine by vLLM's own DP load balancer, so it is
|
|
engine-owned and emits no ``omni_lb_policy`` (that string configures omni's
|
|
*replica* balancer, a different layer). Key-stable (hash) request affinity is
|
|
not something vLLM's DP LB guarantees, so it is rejected rather than silently
|
|
dropped.
|
|
"""
|
|
if owner != "engine":
|
|
_fail(
|
|
f"dp axis {spec.name!r} is engine data parallelism realized intra-engine by "
|
|
f"vLLM's DP load balancer; l1_owner must be 'engine', got {owner!r}. For "
|
|
"omni-coordinator-level request fan-out across independent replicas, use a "
|
|
"'stage_replica' axis."
|
|
)
|
|
if _is_affinity_dp_routing(spec.routing):
|
|
_not_implemented(
|
|
f"dp axis {spec.name!r} requests key-stable (hash) routing, which vLLM's "
|
|
"intra-engine DP load balancer does not guarantee — not supported yet. Use "
|
|
"RouteByStage(random|round_robin|least_queue) for stateless DP balancing."
|
|
)
|
|
if not isinstance(spec.routing, RouteByStage):
|
|
_fail(
|
|
f"dp axis {spec.name!r} expects RouteByStage(random|round_robin|least_queue) routing, "
|
|
f"got {type(spec.routing).__name__}"
|
|
)
|
|
if spec.routing.routing_policy not in _STAGE_POLICY_TO_OMNI_LB:
|
|
# Recognized-but-unimplemented routing (key-stable/hash) is already
|
|
# handled above via _is_affinity_dp_routing -> _not_implemented. Anything
|
|
# left here is an unknown/invalid policy value (e.g. a typo from YAML),
|
|
# i.e. invalid input -> AxisTranslationError, not NotImplementedError.
|
|
_fail(
|
|
f"dp axis {spec.name!r} has invalid routing_policy "
|
|
f"{spec.routing.routing_policy!r}; expected one of "
|
|
f"{sorted(_STAGE_POLICY_TO_OMNI_LB)}."
|
|
)
|
|
|
|
|
|
def _stage_replica_lb_policy(spec: StrategySpec, owner: L1Owner) -> str:
|
|
"""Return the omni StagePool LB policy for a delegated stage_replica axis.
|
|
|
|
A ``stage_replica`` axis is *not* a vLLM world dimension: it stands up N
|
|
independent engine replicas of one pipeline stage, coordinated by the omni
|
|
coordinator and balanced over a StagePool with stateless policies only
|
|
(random / round-robin / least-queue-length). It maps to the per-stage
|
|
``num_replicas`` count plus the pipeline-level ``omni_lb_policy`` string, so
|
|
its ``l1_owner`` must be ``"delegated"`` (omni owns the routing). Key-stable
|
|
(hash) routing is rejected because omni has no key-stable balancer yet.
|
|
"""
|
|
if owner != "delegated":
|
|
_fail(
|
|
f"stage_replica axis {spec.name!r} must be 'delegated' to omni's StagePool load "
|
|
f"balancer; got owner {owner!r}. Replica routing is owned by omni's coordinator."
|
|
)
|
|
|
|
routing = spec.routing
|
|
if _is_affinity_dp_routing(routing):
|
|
_not_implemented(
|
|
f"stage_replica axis {spec.name!r} requests key-stable (hash) routing, which needs a "
|
|
"dedicated load balancer — not implemented yet. Use "
|
|
"RouteByStage(random|round_robin|least_queue) to delegate to omni's load balancer."
|
|
)
|
|
if not isinstance(routing, RouteByStage):
|
|
_fail(
|
|
f"stage_replica axis {spec.name!r} expects RouteByStage(random|round_robin|least_queue) "
|
|
f"routing, got {type(routing).__name__}"
|
|
)
|
|
policy = _STAGE_POLICY_TO_OMNI_LB.get(routing.routing_policy)
|
|
if policy is None:
|
|
_not_implemented(
|
|
f"stage_replica axis {spec.name!r} routing_policy {routing.routing_policy!r} has no omni LB policy"
|
|
)
|
|
return policy
|
|
|
|
|
|
def _validate_tp(spec: StrategySpec, owner: L1Owner) -> None:
|
|
if not isinstance(spec.routing, Broadcast):
|
|
_fail(f"tp axis {spec.name!r} expects Broadcast routing, got {type(spec.routing).__name__}")
|
|
if owner != "engine":
|
|
_fail(f"tp axis {spec.name!r} is realized intra-engine; l1_owner must be 'engine', got {owner!r}")
|
|
|
|
|
|
def _validate_pp(spec: StrategySpec, owner: L1Owner) -> None:
|
|
if not isinstance(spec.routing, PipelineMicrobatch):
|
|
_fail(f"pp axis {spec.name!r} expects PipelineMicrobatch routing, got {type(spec.routing).__name__}")
|
|
if owner != "engine":
|
|
_fail(f"pp axis {spec.name!r} is realized intra-engine; l1_owner must be 'engine', got {owner!r}")
|
|
|
|
|
|
def _validate_ep(spec: StrategySpec, owner: L1Owner) -> None:
|
|
# Dense EP: every rank still sees the whole batch, experts are sharded
|
|
# across ranks inside the engine (sparse MoE all-to-all is a later stage).
|
|
if not isinstance(spec.routing, Broadcast):
|
|
_fail(
|
|
f"ep axis {spec.name!r} expects Broadcast routing (dense expert parallel), "
|
|
f"got {type(spec.routing).__name__}"
|
|
)
|
|
if owner != "engine":
|
|
_fail(f"ep axis {spec.name!r} is realized intra-engine; l1_owner must be 'engine', got {owner!r}")
|
|
|
|
|
|
def translate_strategy_stack(specs: Sequence[StrategySpec]) -> OmniParallelConfig:
|
|
"""Translate a spec stack into an ``OmniParallelConfig``.
|
|
|
|
Supported kinds: dp (engine data parallel), tp, pp, (dense) ep, and
|
|
stage_replica (omni replicas). Raises ``NotImplementedError`` for deferred
|
|
(affinity / key-stable) routing, and :class:`AxisTranslationError` for any
|
|
other invalid spec (a kind not yet translatable, a repeated kind, an owner
|
|
incompatible with the axis kind, or unsupported routing). The EP degree must
|
|
equal tensor_parallel_size * data_parallel_size.
|
|
"""
|
|
sizes: dict[str, int] = {"tensor_parallel_size": 1, "data_parallel_size": 1, "pipeline_parallel_size": 1}
|
|
owners: dict[MeshAxisKind, L1Owner] = {}
|
|
omni_lb_policy: str | None = None
|
|
stage_replica_size = 1
|
|
enable_expert_parallel = False
|
|
ep_size: int | None = None
|
|
|
|
for spec in specs:
|
|
kind = spec.mesh_axis.kind
|
|
if kind not in _SUPPORTED_KINDS:
|
|
_fail(
|
|
f"axis kind {kind!r} is not translatable yet (supported: {list(_SUPPORTED_KINDS)}); "
|
|
"it is designed-for and lands in a later stage"
|
|
)
|
|
if kind in owners:
|
|
_fail(f"axis kind {kind!r} appears more than once in the spec stack")
|
|
|
|
owner = _resolve_l1_owner(spec)
|
|
if kind == "dp":
|
|
_validate_dp(spec, owner)
|
|
elif kind == "tp":
|
|
_validate_tp(spec, owner)
|
|
elif kind == "pp":
|
|
_validate_pp(spec, owner)
|
|
elif kind == "ep":
|
|
_validate_ep(spec, owner)
|
|
enable_expert_parallel = True
|
|
ep_size = spec.mesh_axis.size
|
|
elif kind == "stage_replica":
|
|
omni_lb_policy = _stage_replica_lb_policy(spec, owner)
|
|
stage_replica_size = spec.mesh_axis.size
|
|
|
|
if kind in _AXIS_TO_ENGINE_FIELD:
|
|
sizes[_AXIS_TO_ENGINE_FIELD[kind]] = spec.mesh_axis.size
|
|
owners[kind] = owner
|
|
|
|
if enable_expert_parallel:
|
|
# Dense EP shards experts across exactly the TP*DP ranks, so the declared
|
|
# EP degree must match that product (it is not its own world dimension,
|
|
# and PP is excluded). An EP axis of size 1 is a degenerate no-op that
|
|
# still sets the flag; it is only valid when TP*DP == 1.
|
|
ep_ranks = sizes["tensor_parallel_size"] * sizes["data_parallel_size"]
|
|
if ep_size != ep_ranks:
|
|
_fail(
|
|
f"ep axis size {ep_size} must equal tensor_parallel_size*data_parallel_size "
|
|
f"(={ep_ranks}); dense expert parallelism shards experts across exactly those ranks"
|
|
)
|
|
|
|
return OmniParallelConfig(
|
|
tensor_parallel_size=sizes["tensor_parallel_size"],
|
|
data_parallel_size=sizes["data_parallel_size"],
|
|
pipeline_parallel_size=sizes["pipeline_parallel_size"],
|
|
enable_expert_parallel=enable_expert_parallel,
|
|
stage_replica_size=stage_replica_size,
|
|
omni_lb_policy=omni_lb_policy,
|
|
l1_owners=owners,
|
|
)
|