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>
196 lines
6.3 KiB
Python
196 lines
6.3 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""Tests for StrategySpec -> OmniParallelConfig translation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
|
|
import pytest
|
|
|
|
from vllm_omni.config.composable_parallel import (
|
|
AxisTranslationError,
|
|
Broadcast,
|
|
FanInByStage,
|
|
MeshAxisSpec,
|
|
PartitionByHash,
|
|
PipelineMicrobatch,
|
|
RouteByStage,
|
|
StrategySpec,
|
|
TakeRank,
|
|
Union,
|
|
translate_strategy_stack,
|
|
)
|
|
from vllm_omni.config.composable_parallel.aggregation import StitchPipeline
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
|
|
|
|
def _tp(size: int) -> StrategySpec:
|
|
return StrategySpec("tp", MeshAxisSpec("tp", size), Broadcast(), TakeRank())
|
|
|
|
|
|
def _dp(size: int, policy: str = "round_robin") -> StrategySpec:
|
|
return StrategySpec("dp", MeshAxisSpec("dp", size), RouteByStage(policy), Union())
|
|
|
|
|
|
def _pp(size: int) -> StrategySpec:
|
|
return StrategySpec("pp", MeshAxisSpec("pp", size), PipelineMicrobatch(), StitchPipeline())
|
|
|
|
|
|
def _ep(size: int) -> StrategySpec:
|
|
return StrategySpec("ep", MeshAxisSpec("ep", size), Broadcast(), Union())
|
|
|
|
|
|
def _stage_replica(size: int, policy: str = "round_robin") -> StrategySpec:
|
|
return StrategySpec("stage_replica", MeshAxisSpec("stage_replica", size), RouteByStage(policy), FanInByStage())
|
|
|
|
|
|
def test_tp_only():
|
|
cfg = translate_strategy_stack([_tp(2)])
|
|
assert cfg.tensor_parallel_size == 2
|
|
assert cfg.data_parallel_size == 1
|
|
assert cfg.world_size == 2
|
|
assert cfg.stage_replica_size == 1
|
|
assert cfg.l1_owners == {"tp": "engine"}
|
|
|
|
|
|
def test_dp_is_engine_owned():
|
|
cfg = translate_strategy_stack([_dp(2)])
|
|
assert cfg.data_parallel_size == 2
|
|
assert cfg.world_size == 2
|
|
# engine DP does not emit an omni LB policy (vLLM's internal DP LB is used).
|
|
assert cfg.omni_lb_policy is None
|
|
assert cfg.as_engine_kwargs().get("omni_lb_policy") is None
|
|
assert cfg.l1_owners["dp"] == "engine"
|
|
|
|
|
|
def test_tp_times_dp_world_size():
|
|
cfg = translate_strategy_stack([_tp(2), _dp(2)])
|
|
assert cfg.world_size == 4
|
|
assert cfg.tensor_parallel_size == 2
|
|
assert cfg.data_parallel_size == 2
|
|
|
|
|
|
def test_stage_replica_is_delegated():
|
|
cfg = translate_strategy_stack([_stage_replica(3, "least_queue")])
|
|
# stage_replica is NOT a world dimension.
|
|
assert cfg.world_size == 1
|
|
assert cfg.stage_replica_size == 3
|
|
assert cfg.omni_lb_policy == "least-queue-length"
|
|
assert cfg.l1_owners["stage_replica"] == "delegated"
|
|
# stage_replica_size is not an engine kwarg; it's a per-stage deploy knob.
|
|
assert "stage_replica_size" not in cfg.as_engine_kwargs()
|
|
|
|
|
|
def test_stage_replica_lb_policy_only_when_replicated():
|
|
cfg = translate_strategy_stack([_stage_replica(1)])
|
|
assert cfg.stage_replica_size == 1
|
|
assert cfg.as_engine_kwargs().get("omni_lb_policy") is None
|
|
|
|
|
|
def test_tp_with_stage_replica():
|
|
cfg = translate_strategy_stack([_tp(2), _stage_replica(2)])
|
|
assert cfg.world_size == 2 # tp only
|
|
assert cfg.tensor_parallel_size == 2
|
|
assert cfg.stage_replica_size == 2
|
|
assert cfg.omni_lb_policy == "round-robin"
|
|
|
|
|
|
def test_ep_must_match_tp_dp_product():
|
|
cfg = translate_strategy_stack([_tp(2), _ep(2)])
|
|
assert cfg.enable_expert_parallel is True
|
|
assert cfg.as_engine_kwargs()["enable_expert_parallel"] is True
|
|
|
|
|
|
def test_ep_mismatch_raises():
|
|
with pytest.raises(Exception) as exc:
|
|
translate_strategy_stack([_tp(2), _ep(4)])
|
|
assert "must equal" in str(exc.value)
|
|
|
|
|
|
def test_duplicate_kind_rejected():
|
|
with pytest.raises(AxisTranslationError):
|
|
translate_strategy_stack([_tp(2), _tp(2)])
|
|
|
|
|
|
def test_unsupported_kind_rejected():
|
|
spec = StrategySpec("sp", MeshAxisSpec("sp_ulysses", 2), Broadcast(), TakeRank())
|
|
with pytest.raises(AxisTranslationError):
|
|
translate_strategy_stack([spec])
|
|
|
|
|
|
def test_dp_hash_routing_rejected():
|
|
spec = StrategySpec("dp", MeshAxisSpec("dp", 2), PartitionByHash(), Union())
|
|
with pytest.raises(NotImplementedError):
|
|
translate_strategy_stack([spec])
|
|
|
|
|
|
def test_dp_invalid_routing_policy_rejected():
|
|
spec = StrategySpec("dp", MeshAxisSpec("dp", 2), RouteByStage("bogus"), Union())
|
|
with pytest.raises(AxisTranslationError):
|
|
translate_strategy_stack([spec])
|
|
|
|
|
|
def test_lb_policy_never_in_engine_kwargs_even_when_replicated():
|
|
# omni_lb_policy is pipeline-wide (applied at engine construction), not a
|
|
# per-stage engine arg, so it must never leak into as_engine_kwargs().
|
|
cfg = translate_strategy_stack([_tp(2), _stage_replica(2, "least_queue")])
|
|
assert cfg.omni_lb_policy == "least-queue-length"
|
|
assert "omni_lb_policy" not in cfg.as_engine_kwargs()
|
|
|
|
|
|
def test_stage_replica_hash_routing_rejected():
|
|
spec = StrategySpec("sr", MeshAxisSpec("stage_replica", 2), RouteByStage("hash"), FanInByStage())
|
|
with pytest.raises(NotImplementedError):
|
|
translate_strategy_stack([spec])
|
|
|
|
|
|
def test_dp_wrong_owner_rejected():
|
|
spec = StrategySpec(
|
|
"dp",
|
|
MeshAxisSpec("dp", 2),
|
|
RouteByStage("round_robin"),
|
|
Union(),
|
|
shard_extension={"l1_owner": "delegated"},
|
|
)
|
|
with pytest.raises(AxisTranslationError):
|
|
translate_strategy_stack([spec])
|
|
|
|
|
|
def test_stage_replica_wrong_owner_rejected():
|
|
spec = StrategySpec(
|
|
"sr",
|
|
MeshAxisSpec("stage_replica", 2),
|
|
RouteByStage("round_robin"),
|
|
FanInByStage(),
|
|
shard_extension={"l1_owner": "engine"},
|
|
)
|
|
with pytest.raises(AxisTranslationError):
|
|
translate_strategy_stack([spec])
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"specs",
|
|
[
|
|
[_tp(2)],
|
|
[_dp(2)],
|
|
[_tp(2), _dp(2)],
|
|
[_tp(2), _ep(2)],
|
|
[_tp(2), _stage_replica(2)],
|
|
[_pp(2)],
|
|
],
|
|
)
|
|
def test_as_engine_kwargs_are_valid_omni_engine_args(specs):
|
|
# Guard against desync: every key emitted by as_engine_kwargs() must be a
|
|
# real OmniEngineArgs field, otherwise constructing the per-stage engine args
|
|
# would explode at runtime. Keep this CPU-only by checking field names rather
|
|
# than instantiating the engine.
|
|
from vllm_omni.engine.arg_utils import OmniEngineArgs
|
|
|
|
valid_fields = {f.name for f in dataclasses.fields(OmniEngineArgs)}
|
|
cfg = translate_strategy_stack(specs)
|
|
kwargs = cfg.as_engine_kwargs()
|
|
unknown = set(kwargs) - valid_fields
|
|
assert not unknown, f"as_engine_kwargs emitted non-OmniEngineArgs fields: {unknown}"
|