-
Notifications
You must be signed in to change notification settings - Fork 8.1k
[Serve] [SGLang] PD disaggregation #63741
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
limarkdcunha
wants to merge
42
commits into
ray-project:master
Choose a base branch
from
limarkdcunha:feature/ray-serve-sglang-pd-disaggregation
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
1205202
test: Added Ray Serve SGLang PD disaggregation support
limarkdcunha 2e85e8e
updates as per feedback
limarkdcunha 0f0c2ec
cloud mirror model path fix
limarkdcunha 0cba02a
bootstrap port offset fix
limarkdcunha 0c1a7dd
prefill token clamp fix
limarkdcunha b22e455
DP gang scheduling explicit failure
limarkdcunha db26012
Keep vLLM request-shaping policy out of the neutral connector base
limarkdcunha ba9e702
validation step against mixed engine setup
limarkdcunha e7e9798
Stale engine config fix
limarkdcunha d4d5552
benchmark scripts for testing
limarkdcunha 822cb17
feedback fix
limarkdcunha 448cdde
fix in r_ud
limarkdcunha 7f73224
request id fix
limarkdcunha 9f6194e
fix as per feedback
limarkdcunha 27d6e5e
Changes as per feedback
limarkdcunha a6646b2
Multi GPU collision fix
limarkdcunha a95a1f8
setdefault role correction skip fix
limarkdcunha cf895d2
Added role validator
limarkdcunha 9cc692b
optimization attempt: 1 _concurrent_decode
limarkdcunha 999501f
abort on prefill failures as before
limarkdcunha 37060fa
Stagger native PD server launches to avoid PID limit spike
limarkdcunha 8cc96f9
Added build_asgi_app for sglang
limarkdcunha 1b7bd5d
added tracer logs
limarkdcunha f182e54
updated in benchmark code
limarkdcunha ee6c8fc
tracer code fix
limarkdcunha feee4e7
setting stream_batching_interval_ms to 0
limarkdcunha 86a6633
testing skip slot reservation for thesis testing
limarkdcunha ea41f52
fix in router as per feedback
limarkdcunha b794ee4
forward thread caps to pd replicas always
limarkdcunha 765a056
Undoing experimental changes
limarkdcunha ce0eaf5
reverted legit change
limarkdcunha f3b030e
added trace to measure one specific flow
limarkdcunha d8012a5
resolved as per feedback
limarkdcunha e467352
log queue wait and probe time as aggregates
limarkdcunha 72f085e
Added more logging steps
limarkdcunha c1f01a3
Some more logging
limarkdcunha ff404ac
fix to last logging
limarkdcunha b201b25
Buffer PD trace logging off hot path; fix sweep harness
limarkdcunha 0048e22
Write PD trace rows direct to file; drain on start and exit
limarkdcunha f4207a4
Report server token counts and support ignore_eos in PD benchmarks
limarkdcunha 94f1194
Count output chunks on both arms, not server token counts
limarkdcunha b9d350a
Clean up benchmark code and comments
limarkdcunha File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
67 changes: 67 additions & 0 deletions
67
python/ray/llm/_internal/serve/core/configs/engine_adapter.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| """Per-engine adapters. | ||
|
|
||
| Each adapter declares how an engine names its KV connector and which | ||
| engine-config class builds its ``EngineConfig``. This replaces ``if vLLM / | ||
| elif SGLang`` branching in ``llm_config.py`` so adding an engine is additive: | ||
| add an adapter and register it here. | ||
| """ | ||
|
|
||
| import abc | ||
| from typing import TYPE_CHECKING, Any, Dict, Optional, Type | ||
|
|
||
| if TYPE_CHECKING: | ||
| pass | ||
|
|
||
|
|
||
| class EngineAdapter(abc.ABC): | ||
| @abc.abstractmethod | ||
| def connector_name(self, engine_kwargs: Dict[str, Any]) -> Optional[str]: | ||
| """The KV-connector registry name implied by engine_kwargs, or None.""" | ||
| ... | ||
|
|
||
| @abc.abstractmethod | ||
| def engine_config_cls(self) -> Type: | ||
| """The engine-config class whose ``from_llm_config`` builds the config.""" | ||
| ... | ||
|
|
||
|
|
||
| class VLLMAdapter(EngineAdapter): | ||
| def connector_name(self, engine_kwargs: Dict[str, Any]) -> Optional[str]: | ||
| cfg = engine_kwargs.get("kv_transfer_config") | ||
| if not cfg: | ||
| return None | ||
| kv_connector = cfg.get("kv_connector") | ||
| if not kv_connector: | ||
| # Fail fast: a kv_transfer_config with no kv_connector is a | ||
| # misconfiguration, not a "no connector" case. | ||
| raise ValueError("Connector type is not specified.") | ||
| return kv_connector | ||
|
|
||
| def engine_config_cls(self) -> Type: | ||
| from ray.llm._internal.serve.engines.vllm.vllm_models import VLLMEngineConfig | ||
|
|
||
| return VLLMEngineConfig | ||
|
|
||
|
|
||
| class SGLangAdapter(EngineAdapter): | ||
| def connector_name(self, engine_kwargs: Dict[str, Any]) -> Optional[str]: | ||
| return ( | ||
| "SGLang" if engine_kwargs.get("disaggregation_transfer_backend") else None | ||
| ) | ||
|
|
||
| def engine_config_cls(self) -> Type: | ||
| from ray.llm._internal.serve.engines.sglang.sglang_engine import ( | ||
| SGLangEngineConfig, | ||
| ) | ||
|
|
||
| return SGLangEngineConfig | ||
|
|
||
|
|
||
| _ADAPTERS = {"vLLM": VLLMAdapter, "SGLang": SGLangAdapter} | ||
|
|
||
|
|
||
| def get_engine_adapter(llm_engine: str) -> EngineAdapter: | ||
| try: | ||
| return _ADAPTERS[llm_engine]() | ||
| except KeyError: | ||
| raise ValueError(f"Unsupported engine: {llm_engine}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
176 changes: 176 additions & 0 deletions
176
python/ray/llm/_internal/serve/engines/common/kv_transfer/base.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| import abc | ||
| import random | ||
| import string | ||
| from typing import TYPE_CHECKING, Any, Dict, Optional, Union | ||
|
|
||
| from ray import serve | ||
|
|
||
| if TYPE_CHECKING: | ||
| from ray.llm._internal.serve.core.configs.llm_config import LLMConfig | ||
| from ray.llm._internal.serve.core.configs.openai_api_models import ( | ||
| ChatCompletionRequest, | ||
| CompletionRequest, | ||
| ) | ||
|
|
||
| # The two OpenAI request models the P/D orchestrator shapes. Defined under | ||
| # TYPE_CHECKING (and used as a string annotation) to avoid an import cycle | ||
| # between this module and the config/openai-models modules. | ||
| RequestType = Union[ChatCompletionRequest, CompletionRequest] | ||
|
|
||
|
|
||
| def clamp_request_to_single_token(request: "RequestType") -> None: | ||
| """Clamp a prefill request to a single, non-streaming token (in place).""" | ||
| request.max_tokens = 1 | ||
| if hasattr(request, "max_completion_tokens"): | ||
| request.max_completion_tokens = 1 | ||
| request.stream = False | ||
| if hasattr(request, "stream_options"): | ||
| request.stream_options = None | ||
|
|
||
|
|
||
| class BaseConnectorBackend(abc.ABC): | ||
| # ---- P/D coordination protocol ---- | ||
| # | ||
| # These class attributes and methods let the P/D orchestrator | ||
| # (``PDOrchestratorMixin``) delegate request shaping, peer addressing, and | ||
| # handoff discipline to the connector. They are connector-agnostic: a | ||
| # connector picks a quadrant of (``requires_peer_binding``, | ||
| # ``concurrent_handoff``) and implements ``prepare_prefill_request`` / | ||
| # ``prepare_decode_request`` accordingly. | ||
| # | ||
| # ``requires_peer_binding``: | ||
| # * False -> the orchestrator dispatches prefill via the standard handle | ||
| # path; the peer (if any) is resolved post-hoc from the prefill response. | ||
| # * True -> the orchestrator selects the prefill replica first | ||
| # (``choose_replica``) and passes its ``replica_metadata`` to the backend | ||
| # as ``peer`` (pre-dispatch addressing). | ||
| # | ||
| # ``concurrent_handoff``: | ||
| # * False -> prefill runs to its first chunk before local decode starts | ||
| # (sequential handoff). | ||
| # * True -> prefill dispatch and local decode run concurrently. | ||
| # | ||
| # The two flags are independent; the known combos: | ||
| # * (False, False) — e.g. NixlConnector / LMCacheConnectorV1: decode learns | ||
| # everything it needs (remote engine id / block ids) from the prefill | ||
| # response, so it must wait for that response (sequential). | ||
| # * (True, True) — e.g. MoRIIO WRITE mode / SGLang: the peer address is | ||
| # bound up front and prefill *pushes* KV to decode, so decode needs | ||
| # nothing from prefill's response and can start immediately (concurrent). | ||
| # Concurrent handoff is only possible because peer binding happens up | ||
| # front. | ||
| # * (True, False) — e.g. MoRIIO READ mode: the peer is bound up front, | ||
| # but decode *pulls* KV using block ids returned in prefill's response, | ||
| # so it still waits for prefill to finish (sequential). | ||
| requires_peer_binding: bool = False | ||
| concurrent_handoff: bool = False | ||
|
|
||
| def __init__(self, llm_config: "LLMConfig"): | ||
| """Base class for connector backends. | ||
|
|
||
| Args: | ||
| llm_config: The llm configuration for this engine | ||
| """ | ||
| self.llm_config = llm_config | ||
|
|
||
| def _get_unique_suffix(self, len: int = 6) -> str: | ||
| """Generates unique alphanumeric suffix. | ||
|
|
||
| Args: | ||
| len: Length of the suffix to generate. | ||
| Returns: | ||
| A unique alphanumeric suffix string of specified length. | ||
| """ | ||
| return "".join(random.choices(string.ascii_letters + string.digits, k=len)) | ||
|
|
||
| def _compute_port_offset(self) -> int: | ||
| """Compute a deterministic port offset for this replica. | ||
|
|
||
| Uses data_parallel_rank if DP case, otherwise falls back to | ||
| the replica rank assigned by Ray Serve (TP/PP case). | ||
|
|
||
| For TP/PP cases, multiply by num_devices (tp × pp) to reserve | ||
| sufficient port space, since each worker needs a unique port. | ||
| Each TP worker adds its tp_rank (0, 1, ..., tp_size-1) to the | ||
| base port at bind time, and PP stages also need separate ports. | ||
|
|
||
| ``num_devices`` is read from the neutral ``LLMConfig`` accessor, so this | ||
| base never reaches into an engine-specific config type. | ||
|
|
||
| Returns: | ||
| Non-negative integer offset to add to a base port. | ||
| """ | ||
| # Prefer explicit DP rank when available | ||
| dp_rank = self.llm_config.engine_kwargs.get("data_parallel_rank") | ||
| if isinstance(dp_rank, int) and dp_rank >= 0: | ||
| # vLLM already accounts for TP spacing in DP offset calculation | ||
| # (data_parallel_rank × tp_size), don't multiply here | ||
| return dp_rank | ||
|
|
||
| # NOTE (jeffreywang): A missing replica context must fail loudly, not | ||
| # silently return a 0 offset that collides colocated replicas on the | ||
| # same side-channel port. get_replica_context() raises RayServeException | ||
| # outside a replica. | ||
| rc = serve.get_replica_context() | ||
| num_devices = self.llm_config.num_devices | ||
| return rc.rank.rank * num_devices | ||
|
|
||
| @abc.abstractmethod | ||
| def prepare_prefill_request( | ||
| self, *, request: "RequestType", peer: Optional[Dict[str, Any]] | ||
| ) -> "RequestType": | ||
| """Shape the request sent to the remote prefill engine. | ||
|
|
||
| Args: | ||
| request: The incoming chat/completion request. | ||
| peer: The selected prefill replica's ``replica_metadata`` dict when | ||
| the connector opted into pre-dispatch peer binding | ||
| (``requires_peer_binding=True``), else None. | ||
|
|
||
| Returns: | ||
| A new request object to dispatch to the prefill engine. | ||
| """ | ||
| ... | ||
|
|
||
| @abc.abstractmethod | ||
| def prepare_decode_request( | ||
| self, | ||
| *, | ||
| request: "RequestType", | ||
| peer: Optional[Dict[str, Any]], | ||
| prefill_response: Optional[Any], | ||
| ) -> "RequestType": | ||
| """Shape the request run on the local decode engine. | ||
|
|
||
| Args: | ||
| request: The incoming chat/completion request. | ||
| peer: The selected prefill replica's ``replica_metadata`` dict when | ||
| the connector opted into pre-dispatch peer binding, else None. | ||
| prefill_response: The captured prefill response chunk whose | ||
| ``kv_transfer_params`` may be forwarded, or None when no chunk is | ||
| captured before decode starts (concurrent-handoff mode). | ||
|
|
||
| Returns: | ||
| A new request object to run on the local decode engine. | ||
| """ | ||
| ... | ||
|
|
||
| def setup(self) -> None: | ||
| """Setup the connector backend. | ||
|
|
||
| This method is called to setup the connector backend. | ||
| """ | ||
| pass | ||
|
|
||
| def replica_metadata(self) -> Dict[str, Any]: | ||
| """Static per-replica coordination data published to the orchestrator. | ||
|
|
||
| Surfaced via the replica-metadata hook on ``ReplicaSelection`` so that a | ||
| connector opting into ``requires_peer_binding`` can address the selected | ||
| prefill peer. The default backend publishes nothing; connectors that need | ||
| to advertise an address (e.g. MoRIIO's zmq endpoint) override this. | ||
|
|
||
| Returns: | ||
| A JSON-serializable dict of per-replica metadata (empty by default). | ||
| """ | ||
| return {} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.