[PD-Disagg] Fully support external DP dispatch w/ PD-disaggregation mode. (#19268)
Co-authored-by: Ratish P <114130421+ratish1@users.noreply.github.com>
This commit is contained in:
co-authored by
Ratish P
parent
241ee90164
commit
539f772f54
@@ -355,15 +355,15 @@ class DecodePreallocQueue:
|
|||||||
req.retraction_mb_id = None
|
req.retraction_mb_id = None
|
||||||
self.retracted_queue.append(req)
|
self.retracted_queue.append(req)
|
||||||
else:
|
else:
|
||||||
dp_rank = self._resolve_dp_rank(req)
|
prefill_dp_rank = self._resolve_prefill_dp_rank(req)
|
||||||
if dp_rank is None:
|
if prefill_dp_rank is None:
|
||||||
self.pending_reqs.append(req)
|
self.pending_reqs.append(req)
|
||||||
return
|
return
|
||||||
self._create_receiver_and_enqueue(req, dp_rank)
|
self._create_receiver_and_enqueue(req, prefill_dp_rank)
|
||||||
|
|
||||||
def _resolve_dp_rank(self, req: Req) -> Optional[int]:
|
def _resolve_prefill_dp_rank(self, req: Req) -> Optional[int]:
|
||||||
if req.data_parallel_rank is not None:
|
if req.disagg_prefill_dp_rank is not None:
|
||||||
return req.data_parallel_rank
|
return req.disagg_prefill_dp_rank
|
||||||
|
|
||||||
if _is_fake_transfer(req, self.scheduler.server_args):
|
if _is_fake_transfer(req, self.scheduler.server_args):
|
||||||
return 0
|
return 0
|
||||||
@@ -379,7 +379,7 @@ class DecodePreallocQueue:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _create_receiver_and_enqueue(self, req: Req, dp_rank: int) -> None:
|
def _create_receiver_and_enqueue(self, req: Req, prefill_dp_rank: int) -> None:
|
||||||
backend = (
|
backend = (
|
||||||
TransferBackend.FAKE
|
TransferBackend.FAKE
|
||||||
if _is_fake_transfer(req, self.scheduler.server_args)
|
if _is_fake_transfer(req, self.scheduler.server_args)
|
||||||
@@ -391,7 +391,7 @@ class DecodePreallocQueue:
|
|||||||
mgr=self.kv_manager,
|
mgr=self.kv_manager,
|
||||||
bootstrap_addr=f"{req.bootstrap_host}:{req.bootstrap_port}",
|
bootstrap_addr=f"{req.bootstrap_host}:{req.bootstrap_port}",
|
||||||
bootstrap_room=req.bootstrap_room,
|
bootstrap_room=req.bootstrap_room,
|
||||||
prefill_dp_rank=dp_rank,
|
prefill_dp_rank=prefill_dp_rank,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.queue.append(
|
self.queue.append(
|
||||||
@@ -493,16 +493,16 @@ class DecodePreallocQueue:
|
|||||||
raise ValueError(f"Unexpected poll case: {poll}")
|
raise ValueError(f"Unexpected poll case: {poll}")
|
||||||
|
|
||||||
def _resolve_pending_reqs(self) -> None:
|
def _resolve_pending_reqs(self) -> None:
|
||||||
"""Batch-resolve dp_ranks for pending requests and create receivers."""
|
"""Batch-resolve prefill_dp_ranks for pending requests and create receivers."""
|
||||||
if not self.pending_reqs:
|
if not self.pending_reqs:
|
||||||
return
|
return
|
||||||
|
|
||||||
bootstrap_addr = f"{self.pending_reqs[0].bootstrap_host}:{self.pending_reqs[0].bootstrap_port}"
|
bootstrap_addr = f"{self.pending_reqs[0].bootstrap_host}:{self.pending_reqs[0].bootstrap_port}"
|
||||||
|
|
||||||
# If a request is following the bootstrap room,
|
# If a request is following the bootstrap room,
|
||||||
# we need get the prefill info before resolving the dp_rank,
|
# we need get the prefill info before resolving the prefill_dp_ranks
|
||||||
# which is a conflict with the lazy resolve logic in CommonKVReceiver,
|
# which is a conflict with the lazy resolve logic in CommonKVReceiver,
|
||||||
# so we need to ensure the parallel info before resolving the dp_rank
|
# so we need to ensure the parallel info before resolving it.
|
||||||
if not self.kv_manager.ensure_parallel_info(bootstrap_addr):
|
if not self.kv_manager.ensure_parallel_info(bootstrap_addr):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -510,9 +510,9 @@ class DecodePreallocQueue:
|
|||||||
need_query = []
|
need_query = []
|
||||||
for req in self.pending_reqs:
|
for req in self.pending_reqs:
|
||||||
# NOTE: we need resolve it again because we may ensure the parallel info here
|
# NOTE: we need resolve it again because we may ensure the parallel info here
|
||||||
dp_rank = self._resolve_dp_rank(req)
|
prefill_dp_rank = self._resolve_prefill_dp_rank(req)
|
||||||
if dp_rank is not None:
|
if prefill_dp_rank is not None:
|
||||||
resolved.append((req, dp_rank))
|
resolved.append((req, prefill_dp_rank))
|
||||||
else:
|
else:
|
||||||
need_query.append(req)
|
need_query.append(req)
|
||||||
|
|
||||||
@@ -534,8 +534,8 @@ class DecodePreallocQueue:
|
|||||||
else:
|
else:
|
||||||
self.pending_reqs = []
|
self.pending_reqs = []
|
||||||
|
|
||||||
for req, dp_rank in resolved:
|
for req, prefill_dp_rank in resolved:
|
||||||
self._create_receiver_and_enqueue(req, dp_rank)
|
self._create_receiver_and_enqueue(req, prefill_dp_rank)
|
||||||
|
|
||||||
def pop_preallocated(
|
def pop_preallocated(
|
||||||
self, rids_to_check: Optional[List[str]] = None
|
self, rids_to_check: Optional[List[str]] = None
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ class MMReceiverHTTP(MMReceiverBase):
|
|||||||
skip_mm_pool=True,
|
skip_mm_pool=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_req(self, recv_req):
|
def create_req(self, recv_req: TokenizedGenerateReqInput):
|
||||||
req = Req(
|
req = Req(
|
||||||
recv_req.rid,
|
recv_req.rid,
|
||||||
recv_req.input_text,
|
recv_req.input_text,
|
||||||
@@ -362,7 +362,8 @@ class MMReceiverHTTP(MMReceiverBase):
|
|||||||
bootstrap_port=recv_req.bootstrap_port,
|
bootstrap_port=recv_req.bootstrap_port,
|
||||||
bootstrap_room=recv_req.bootstrap_room,
|
bootstrap_room=recv_req.bootstrap_room,
|
||||||
disagg_mode=self.scheduler.disaggregation_mode,
|
disagg_mode=self.scheduler.disaggregation_mode,
|
||||||
data_parallel_rank=recv_req.data_parallel_rank,
|
routed_dp_rank=recv_req.routed_dp_rank,
|
||||||
|
disagg_prefill_dp_rank=recv_req.disagg_prefill_dp_rank,
|
||||||
vocab_size=self.scheduler.model_config.vocab_size,
|
vocab_size=self.scheduler.model_config.vocab_size,
|
||||||
priority=recv_req.priority,
|
priority=recv_req.priority,
|
||||||
metrics_collector=(
|
metrics_collector=(
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ class EngineBase(ABC):
|
|||||||
bootstrap_host: Optional[Union[List[str], str]] = None,
|
bootstrap_host: Optional[Union[List[str], str]] = None,
|
||||||
bootstrap_port: Optional[Union[List[int], int]] = None,
|
bootstrap_port: Optional[Union[List[int], int]] = None,
|
||||||
bootstrap_room: Optional[Union[List[int], int]] = None,
|
bootstrap_room: Optional[Union[List[int], int]] = None,
|
||||||
|
routed_dp_rank: Optional[int] = None,
|
||||||
|
disagg_prefill_dp_rank: Optional[int] = None,
|
||||||
data_parallel_rank: Optional[int] = None,
|
data_parallel_rank: Optional[int] = None,
|
||||||
rid: Optional[Union[List[str], str]] = None,
|
rid: Optional[Union[List[str], str]] = None,
|
||||||
priority: Optional[int] = None,
|
priority: Optional[int] = None,
|
||||||
|
|||||||
@@ -202,6 +202,35 @@ class Engine(EngineBase):
|
|||||||
self.loop = asyncio.new_event_loop()
|
self.loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(self.loop)
|
asyncio.set_event_loop(self.loop)
|
||||||
|
|
||||||
|
def _resolve_routed_dp_rank(
|
||||||
|
self,
|
||||||
|
routed_dp_rank: Optional[int],
|
||||||
|
data_parallel_rank: Optional[int],
|
||||||
|
) -> Optional[int]:
|
||||||
|
if data_parallel_rank is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'data_parallel_rank' is deprecated, use 'routed_dp_rank' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=3,
|
||||||
|
)
|
||||||
|
if routed_dp_rank is None:
|
||||||
|
routed_dp_rank = data_parallel_rank
|
||||||
|
|
||||||
|
if self.server_args.enable_dp_attention:
|
||||||
|
if routed_dp_rank is None:
|
||||||
|
logger.debug("routed_dp_rank not provided, using default dispatch")
|
||||||
|
elif routed_dp_rank < 0:
|
||||||
|
raise ValueError("routed_dp_rank must be non-negative")
|
||||||
|
elif routed_dp_rank >= self.server_args.dp_size:
|
||||||
|
raise ValueError(
|
||||||
|
f"routed_dp_rank must be less than dp_size: {self.server_args.dp_size}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"routed_dp_rank: {routed_dp_rank}")
|
||||||
|
return routed_dp_rank
|
||||||
|
|
||||||
def generate(
|
def generate(
|
||||||
self,
|
self,
|
||||||
# The input prompt. It can be a single prompt or a batch of prompts.
|
# The input prompt. It can be a single prompt or a batch of prompts.
|
||||||
@@ -232,6 +261,9 @@ class Engine(EngineBase):
|
|||||||
bootstrap_host: Optional[Union[List[str], str]] = None,
|
bootstrap_host: Optional[Union[List[str], str]] = None,
|
||||||
bootstrap_port: Optional[Union[List[int], int]] = None,
|
bootstrap_port: Optional[Union[List[int], int]] = None,
|
||||||
bootstrap_room: Optional[Union[List[int], int]] = None,
|
bootstrap_room: Optional[Union[List[int], int]] = None,
|
||||||
|
routed_dp_rank: Optional[int] = None,
|
||||||
|
disagg_prefill_dp_rank: Optional[int] = None,
|
||||||
|
# Deprecated: use routed_dp_rank instead
|
||||||
data_parallel_rank: Optional[int] = None,
|
data_parallel_rank: Optional[int] = None,
|
||||||
external_trace_header: Optional[Dict] = None,
|
external_trace_header: Optional[Dict] = None,
|
||||||
rid: Optional[Union[List[str], str]] = None,
|
rid: Optional[Union[List[str], str]] = None,
|
||||||
@@ -242,14 +274,8 @@ class Engine(EngineBase):
|
|||||||
The arguments of this function is the same as `sglang/srt/managers/io_struct.py::GenerateReqInput`.
|
The arguments of this function is the same as `sglang/srt/managers/io_struct.py::GenerateReqInput`.
|
||||||
Please refer to `GenerateReqInput` for the documentation.
|
Please refer to `GenerateReqInput` for the documentation.
|
||||||
"""
|
"""
|
||||||
if self.server_args.enable_dp_attention:
|
routed_dp_rank = self._resolve_routed_dp_rank(
|
||||||
if data_parallel_rank is None:
|
routed_dp_rank, data_parallel_rank
|
||||||
logger.debug("data_parallel_rank not provided, using default dispatch")
|
|
||||||
elif data_parallel_rank < 0:
|
|
||||||
raise ValueError("data_parallel_rank must be non-negative")
|
|
||||||
elif data_parallel_rank >= self.server_args.dp_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"data_parallel_rank must be less than dp_size: {self.server_args.dp_size}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
obj = GenerateReqInput(
|
obj = GenerateReqInput(
|
||||||
@@ -271,7 +297,8 @@ class Engine(EngineBase):
|
|||||||
bootstrap_host=bootstrap_host,
|
bootstrap_host=bootstrap_host,
|
||||||
bootstrap_port=bootstrap_port,
|
bootstrap_port=bootstrap_port,
|
||||||
bootstrap_room=bootstrap_room,
|
bootstrap_room=bootstrap_room,
|
||||||
data_parallel_rank=data_parallel_rank,
|
routed_dp_rank=routed_dp_rank,
|
||||||
|
disagg_prefill_dp_rank=disagg_prefill_dp_rank,
|
||||||
external_trace_header=external_trace_header,
|
external_trace_header=external_trace_header,
|
||||||
rid=rid,
|
rid=rid,
|
||||||
session_params=session_params,
|
session_params=session_params,
|
||||||
@@ -324,6 +351,9 @@ class Engine(EngineBase):
|
|||||||
bootstrap_host: Optional[Union[List[str], str]] = None,
|
bootstrap_host: Optional[Union[List[str], str]] = None,
|
||||||
bootstrap_port: Optional[Union[List[int], int]] = None,
|
bootstrap_port: Optional[Union[List[int], int]] = None,
|
||||||
bootstrap_room: Optional[Union[List[int], int]] = None,
|
bootstrap_room: Optional[Union[List[int], int]] = None,
|
||||||
|
routed_dp_rank: Optional[int] = None,
|
||||||
|
disagg_prefill_dp_rank: Optional[int] = None,
|
||||||
|
# Deprecated: use routed_dp_rank instead
|
||||||
data_parallel_rank: Optional[int] = None,
|
data_parallel_rank: Optional[int] = None,
|
||||||
external_trace_header: Optional[Dict] = None,
|
external_trace_header: Optional[Dict] = None,
|
||||||
rid: Optional[Union[List[str], str]] = None,
|
rid: Optional[Union[List[str], str]] = None,
|
||||||
@@ -334,18 +364,10 @@ class Engine(EngineBase):
|
|||||||
The arguments of this function is the same as `sglang/srt/managers/io_struct.py::GenerateReqInput`.
|
The arguments of this function is the same as `sglang/srt/managers/io_struct.py::GenerateReqInput`.
|
||||||
Please refer to `GenerateReqInput` for the documentation.
|
Please refer to `GenerateReqInput` for the documentation.
|
||||||
"""
|
"""
|
||||||
|
routed_dp_rank = self._resolve_routed_dp_rank(
|
||||||
if self.server_args.enable_dp_attention:
|
routed_dp_rank, data_parallel_rank
|
||||||
if data_parallel_rank is None:
|
|
||||||
logger.debug("data_parallel_rank not provided, using default dispatch")
|
|
||||||
elif data_parallel_rank < 0:
|
|
||||||
raise ValueError("data_parallel_rank must be non-negative")
|
|
||||||
elif data_parallel_rank >= self.server_args.dp_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"data_parallel_rank must be in range [0, {self.server_args.dp_size-1}]"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"data_parallel_rank: {data_parallel_rank}")
|
|
||||||
obj = GenerateReqInput(
|
obj = GenerateReqInput(
|
||||||
text=prompt,
|
text=prompt,
|
||||||
input_ids=input_ids,
|
input_ids=input_ids,
|
||||||
@@ -365,7 +387,8 @@ class Engine(EngineBase):
|
|||||||
bootstrap_host=bootstrap_host,
|
bootstrap_host=bootstrap_host,
|
||||||
bootstrap_port=bootstrap_port,
|
bootstrap_port=bootstrap_port,
|
||||||
bootstrap_room=bootstrap_room,
|
bootstrap_room=bootstrap_room,
|
||||||
data_parallel_rank=data_parallel_rank,
|
routed_dp_rank=routed_dp_rank,
|
||||||
|
disagg_prefill_dp_rank=disagg_prefill_dp_rank,
|
||||||
external_trace_header=external_trace_header,
|
external_trace_header=external_trace_header,
|
||||||
rid=rid,
|
rid=rid,
|
||||||
session_params=session_params,
|
session_params=session_params,
|
||||||
|
|||||||
@@ -233,6 +233,20 @@ class BatchResponse(BaseModel):
|
|||||||
metadata: Optional[dict] = None
|
metadata: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_deprecated_dp_rank(values: dict) -> dict:
|
||||||
|
if isinstance(values, dict) and values.get("data_parallel_rank") is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'data_parallel_rank' is deprecated, use 'routed_dp_rank' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
if values.get("routed_dp_rank") is None:
|
||||||
|
values["routed_dp_rank"] = values["data_parallel_rank"]
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
class CompletionRequest(BaseModel):
|
class CompletionRequest(BaseModel):
|
||||||
# Ordered by official OpenAI API documentation
|
# Ordered by official OpenAI API documentation
|
||||||
# https://platform.openai.com/docs/api-reference/completions/create
|
# https://platform.openai.com/docs/api-reference/completions/create
|
||||||
@@ -285,7 +299,11 @@ class CompletionRequest(BaseModel):
|
|||||||
bootstrap_port: Optional[Union[List[Optional[int]], int]] = None
|
bootstrap_port: Optional[Union[List[Optional[int]], int]] = None
|
||||||
bootstrap_room: Optional[Union[List[int], int]] = None
|
bootstrap_room: Optional[Union[List[int], int]] = None
|
||||||
|
|
||||||
# For data parallel rank routing
|
# For DP routing — external router assigns a specific DP worker
|
||||||
|
routed_dp_rank: Optional[int] = None
|
||||||
|
# For PD disagg — hint telling decode which prefill DP worker has the KV cache
|
||||||
|
disagg_prefill_dp_rank: Optional[int] = None
|
||||||
|
# Deprecated: use routed_dp_rank instead
|
||||||
data_parallel_rank: Optional[int] = None
|
data_parallel_rank: Optional[int] = None
|
||||||
|
|
||||||
# For request id
|
# For request id
|
||||||
@@ -300,6 +318,11 @@ class CompletionRequest(BaseModel):
|
|||||||
# For custom metric labels
|
# For custom metric labels
|
||||||
custom_labels: Optional[Dict[str, str]] = None
|
custom_labels: Optional[Dict[str, str]] = None
|
||||||
|
|
||||||
|
@model_validator(mode="before")
|
||||||
|
@classmethod
|
||||||
|
def _handle_deprecated_dp_rank(cls, values):
|
||||||
|
return _migrate_deprecated_dp_rank(values)
|
||||||
|
|
||||||
@field_validator("max_tokens")
|
@field_validator("max_tokens")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_max_tokens_positive(cls, v):
|
def validate_max_tokens_positive(cls, v):
|
||||||
@@ -614,7 +637,11 @@ class ChatCompletionRequest(BaseModel):
|
|||||||
bootstrap_port: Optional[Union[List[Optional[int]], int]] = None
|
bootstrap_port: Optional[Union[List[Optional[int]], int]] = None
|
||||||
bootstrap_room: Optional[Union[List[int], int]] = None
|
bootstrap_room: Optional[Union[List[int], int]] = None
|
||||||
|
|
||||||
# For data parallel rank routing
|
# For DP routing — external router assigns a specific DP worker
|
||||||
|
routed_dp_rank: Optional[int] = None
|
||||||
|
# For PD disagg — hint telling decode which prefill DP worker has the KV cache
|
||||||
|
disagg_prefill_dp_rank: Optional[int] = None
|
||||||
|
# Deprecated: use routed_dp_rank instead
|
||||||
data_parallel_rank: Optional[int] = None
|
data_parallel_rank: Optional[int] = None
|
||||||
|
|
||||||
# OpenAI/SGLang default sampling parameters
|
# OpenAI/SGLang default sampling parameters
|
||||||
@@ -626,6 +653,11 @@ class ChatCompletionRequest(BaseModel):
|
|||||||
"repetition_penalty": 1.0,
|
"repetition_penalty": 1.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@model_validator(mode="before")
|
||||||
|
@classmethod
|
||||||
|
def _handle_deprecated_dp_rank(cls, values):
|
||||||
|
return _migrate_deprecated_dp_rank(values)
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_tool_choice_default(cls, values):
|
def set_tool_choice_default(cls, values):
|
||||||
|
|||||||
@@ -296,7 +296,8 @@ class OpenAIServingChat(OpenAIServingBase):
|
|||||||
bootstrap_host=request.bootstrap_host,
|
bootstrap_host=request.bootstrap_host,
|
||||||
bootstrap_port=request.bootstrap_port,
|
bootstrap_port=request.bootstrap_port,
|
||||||
bootstrap_room=request.bootstrap_room,
|
bootstrap_room=request.bootstrap_room,
|
||||||
data_parallel_rank=request.data_parallel_rank,
|
routed_dp_rank=request.routed_dp_rank,
|
||||||
|
disagg_prefill_dp_rank=request.disagg_prefill_dp_rank,
|
||||||
return_hidden_states=request.return_hidden_states,
|
return_hidden_states=request.return_hidden_states,
|
||||||
return_routed_experts=request.return_routed_experts,
|
return_routed_experts=request.return_routed_experts,
|
||||||
rid=request.rid,
|
rid=request.rid,
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ class OpenAIServingCompletion(OpenAIServingBase):
|
|||||||
bootstrap_host=request.bootstrap_host,
|
bootstrap_host=request.bootstrap_host,
|
||||||
bootstrap_port=request.bootstrap_port,
|
bootstrap_port=request.bootstrap_port,
|
||||||
bootstrap_room=request.bootstrap_room,
|
bootstrap_room=request.bootstrap_room,
|
||||||
data_parallel_rank=request.data_parallel_rank,
|
routed_dp_rank=request.routed_dp_rank,
|
||||||
|
disagg_prefill_dp_rank=request.disagg_prefill_dp_rank,
|
||||||
return_hidden_states=request.return_hidden_states,
|
return_hidden_states=request.return_hidden_states,
|
||||||
return_routed_experts=request.return_routed_experts,
|
return_routed_experts=request.return_routed_experts,
|
||||||
rid=request.rid,
|
rid=request.rid,
|
||||||
|
|||||||
@@ -494,9 +494,9 @@ class DataParallelController:
|
|||||||
self.max_req_input_len = scheduler_info[0]["max_req_input_len"]
|
self.max_req_input_len = scheduler_info[0]["max_req_input_len"]
|
||||||
|
|
||||||
def maybe_external_dp_rank_routing(self, req: Req):
|
def maybe_external_dp_rank_routing(self, req: Req):
|
||||||
if req.data_parallel_rank is not None:
|
if req.routed_dp_rank is not None:
|
||||||
logger.debug(f"Direct routing to DP rank {req.data_parallel_rank}")
|
logger.debug(f"Direct routing to DP rank {req.routed_dp_rank}")
|
||||||
self.workers[req.data_parallel_rank].send_pyobj(req)
|
self.workers[req.routed_dp_rank].send_pyobj(req)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -400,6 +400,7 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin):
|
|||||||
retraction_counts=recv_obj.retraction_counts,
|
retraction_counts=recv_obj.retraction_counts,
|
||||||
token_steps=recv_obj.token_steps,
|
token_steps=recv_obj.token_steps,
|
||||||
load=recv_obj.load,
|
load=recv_obj.load,
|
||||||
|
dp_ranks=recv_obj.dp_ranks,
|
||||||
time_stats=recv_obj.time_stats,
|
time_stats=recv_obj.time_stats,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -187,7 +187,11 @@ class GenerateReqInput(BaseReq):
|
|||||||
# Require reasoning for the request (hybrid reasoning model only)
|
# Require reasoning for the request (hybrid reasoning model only)
|
||||||
require_reasoning: bool = False
|
require_reasoning: bool = False
|
||||||
|
|
||||||
# For data parallel rank routing
|
# For DP routing — external router assigns a specific DP worker
|
||||||
|
routed_dp_rank: Optional[int] = None
|
||||||
|
# For PD disagg — hint telling decode which prefill DP worker has the KV cache
|
||||||
|
disagg_prefill_dp_rank: Optional[int] = None
|
||||||
|
# Deprecated: use routed_dp_rank instead
|
||||||
data_parallel_rank: Optional[int] = None
|
data_parallel_rank: Optional[int] = None
|
||||||
|
|
||||||
# For background responses (OpenAI responses API)
|
# For background responses (OpenAI responses API)
|
||||||
@@ -251,6 +255,18 @@ class GenerateReqInput(BaseReq):
|
|||||||
ValueError: If inputs are not properly specified (e.g., none or all of
|
ValueError: If inputs are not properly specified (e.g., none or all of
|
||||||
text, input_ids, input_embeds are provided)
|
text, input_ids, input_embeds are provided)
|
||||||
"""
|
"""
|
||||||
|
if self.data_parallel_rank is not None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'data_parallel_rank' is deprecated, use 'routed_dp_rank' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
if self.routed_dp_rank is None:
|
||||||
|
self.routed_dp_rank = self.data_parallel_rank
|
||||||
|
self.data_parallel_rank = None
|
||||||
|
|
||||||
self._validate_inputs()
|
self._validate_inputs()
|
||||||
self._determine_batch_size()
|
self._determine_batch_size()
|
||||||
self._handle_parallel_sampling()
|
self._handle_parallel_sampling()
|
||||||
@@ -624,9 +640,8 @@ class GenerateReqInput(BaseReq):
|
|||||||
decode_tp_size=(
|
decode_tp_size=(
|
||||||
self.decode_tp_size[i] if self.decode_tp_size is not None else None
|
self.decode_tp_size[i] if self.decode_tp_size is not None else None
|
||||||
),
|
),
|
||||||
data_parallel_rank=(
|
routed_dp_rank=self.routed_dp_rank,
|
||||||
self.data_parallel_rank if self.data_parallel_rank is not None else None
|
disagg_prefill_dp_rank=self.disagg_prefill_dp_rank,
|
||||||
),
|
|
||||||
conversation_id=self.conversation_id,
|
conversation_id=self.conversation_id,
|
||||||
priority=self.priority,
|
priority=self.priority,
|
||||||
extra_key=self.extra_key,
|
extra_key=self.extra_key,
|
||||||
@@ -693,8 +708,10 @@ class TokenizedGenerateReqInput(BaseReq):
|
|||||||
# Require reasoning for the request (hybrid reasoning model only)
|
# Require reasoning for the request (hybrid reasoning model only)
|
||||||
require_reasoning: bool = False
|
require_reasoning: bool = False
|
||||||
|
|
||||||
# For data parallel rank routing
|
# For DP routing
|
||||||
data_parallel_rank: Optional[int] = None
|
routed_dp_rank: Optional[int] = None
|
||||||
|
# For PD disagg — hint telling decode which prefill DP worker has the KV cache
|
||||||
|
disagg_prefill_dp_rank: Optional[int] = None
|
||||||
|
|
||||||
# Priority for the request
|
# Priority for the request
|
||||||
priority: Optional[int] = None
|
priority: Optional[int] = None
|
||||||
@@ -897,8 +914,8 @@ class TokenizedEmbeddingReqInput(BaseReq):
|
|||||||
token_type_ids: List[int]
|
token_type_ids: List[int]
|
||||||
# Dummy sampling params for compatibility
|
# Dummy sampling params for compatibility
|
||||||
sampling_params: SamplingParams
|
sampling_params: SamplingParams
|
||||||
# For data parallel rank routing
|
# For DP routing
|
||||||
data_parallel_rank: Optional[int] = None
|
routed_dp_rank: Optional[int] = None
|
||||||
# Priority for the request
|
# Priority for the request
|
||||||
priority: Optional[int] = None
|
priority: Optional[int] = None
|
||||||
# The number of dimensions the resulting output embeddings should have. It is applicable for Matryoshka Embeddings.
|
# The number of dimensions the resulting output embeddings should have. It is applicable for Matryoshka Embeddings.
|
||||||
@@ -984,6 +1001,8 @@ class BatchTokenIDOutput(BaseBatchReq, SpeculativeDecodingMetricsMixin):
|
|||||||
customized_info: Optional[Dict[str, List[Any]]] = None
|
customized_info: Optional[Dict[str, List[Any]]] = None
|
||||||
# Detailed breakdown of cached tokens by source (device/host/storage)
|
# Detailed breakdown of cached tokens by source (device/host/storage)
|
||||||
cached_tokens_details: Optional[List[Optional[Dict[str, Any]]]] = None
|
cached_tokens_details: Optional[List[Optional[Dict[str, Any]]]] = None
|
||||||
|
# DP rank of the scheduler that processed each request
|
||||||
|
dp_ranks: Optional[List[int]] = None
|
||||||
|
|
||||||
# For observability
|
# For observability
|
||||||
time_stats: Optional[List[SchedulerReqTimeStats]] = None
|
time_stats: Optional[List[SchedulerReqTimeStats]] = None
|
||||||
@@ -1076,6 +1095,8 @@ class BatchStrOutput(BaseBatchReq, SpeculativeDecodingMetricsMixin):
|
|||||||
customized_info: Optional[Dict[str, List[Any]]] = None
|
customized_info: Optional[Dict[str, List[Any]]] = None
|
||||||
# Detailed breakdown of cached tokens by source (device/host/storage)
|
# Detailed breakdown of cached tokens by source (device/host/storage)
|
||||||
cached_tokens_details: Optional[List[Optional[Dict[str, Any]]]] = None
|
cached_tokens_details: Optional[List[Optional[Dict[str, Any]]]] = None
|
||||||
|
# DP rank of the scheduler that processed each request
|
||||||
|
dp_ranks: Optional[List[int]] = None
|
||||||
|
|
||||||
# For observability
|
# For observability
|
||||||
time_stats: Optional[List[SchedulerReqTimeStats]] = None
|
time_stats: Optional[List[SchedulerReqTimeStats]] = None
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ def _handle_output_by_index(output, i):
|
|||||||
customized_info=_extract_field_by_index(
|
customized_info=_extract_field_by_index(
|
||||||
output, "customized_info", i, check_length=False
|
output, "customized_info", i, check_length=False
|
||||||
),
|
),
|
||||||
|
dp_ranks=_extract_field_by_index(output, "dp_ranks", i, check_length=False),
|
||||||
placeholder_tokens_idx=None,
|
placeholder_tokens_idx=None,
|
||||||
placeholder_tokens_val=None,
|
placeholder_tokens_val=None,
|
||||||
retraction_counts=_extract_field_by_index(output, "retraction_counts", i),
|
retraction_counts=_extract_field_by_index(output, "retraction_counts", i),
|
||||||
|
|||||||
@@ -509,7 +509,8 @@ class Req(ReqDllmMixin):
|
|||||||
bootstrap_port: Optional[int] = None,
|
bootstrap_port: Optional[int] = None,
|
||||||
bootstrap_room: Optional[int] = None,
|
bootstrap_room: Optional[int] = None,
|
||||||
disagg_mode: Optional[DisaggregationMode] = None,
|
disagg_mode: Optional[DisaggregationMode] = None,
|
||||||
data_parallel_rank: Optional[int] = None,
|
routed_dp_rank: Optional[int] = None,
|
||||||
|
disagg_prefill_dp_rank: Optional[int] = None,
|
||||||
vocab_size: Optional[int] = None,
|
vocab_size: Optional[int] = None,
|
||||||
priority: Optional[int] = None,
|
priority: Optional[int] = None,
|
||||||
metrics_collector: Optional[SchedulerMetricsCollector] = None,
|
metrics_collector: Optional[SchedulerMetricsCollector] = None,
|
||||||
@@ -770,8 +771,8 @@ class Req(ReqDllmMixin):
|
|||||||
self.bootstrap_room: Optional[int] = bootstrap_room
|
self.bootstrap_room: Optional[int] = bootstrap_room
|
||||||
self.disagg_kv_sender: Optional[BaseKVSender] = None
|
self.disagg_kv_sender: Optional[BaseKVSender] = None
|
||||||
|
|
||||||
# For data parallel rank routing
|
self.routed_dp_rank: Optional[int] = routed_dp_rank
|
||||||
self.data_parallel_rank: Optional[int] = data_parallel_rank
|
self.disagg_prefill_dp_rank: Optional[int] = disagg_prefill_dp_rank
|
||||||
|
|
||||||
# the start index of the sent kv cache
|
# the start index of the sent kv cache
|
||||||
# We want to send it chunk by chunk for chunked prefill.
|
# We want to send it chunk by chunk for chunked prefill.
|
||||||
|
|||||||
@@ -1505,7 +1505,8 @@ class Scheduler(
|
|||||||
bootstrap_port=recv_req.bootstrap_port,
|
bootstrap_port=recv_req.bootstrap_port,
|
||||||
bootstrap_room=recv_req.bootstrap_room,
|
bootstrap_room=recv_req.bootstrap_room,
|
||||||
disagg_mode=self.disaggregation_mode,
|
disagg_mode=self.disaggregation_mode,
|
||||||
data_parallel_rank=recv_req.data_parallel_rank,
|
routed_dp_rank=recv_req.routed_dp_rank,
|
||||||
|
disagg_prefill_dp_rank=recv_req.disagg_prefill_dp_rank,
|
||||||
vocab_size=self.model_config.vocab_size,
|
vocab_size=self.model_config.vocab_size,
|
||||||
priority=recv_req.priority,
|
priority=recv_req.priority,
|
||||||
metrics_collector=(
|
metrics_collector=(
|
||||||
@@ -1811,6 +1812,7 @@ class Scheduler(
|
|||||||
recv_req.input_ids,
|
recv_req.input_ids,
|
||||||
recv_req.sampling_params,
|
recv_req.sampling_params,
|
||||||
token_type_ids=recv_req.token_type_ids,
|
token_type_ids=recv_req.token_type_ids,
|
||||||
|
routed_dp_rank=recv_req.routed_dp_rank,
|
||||||
priority=recv_req.priority,
|
priority=recv_req.priority,
|
||||||
dimensions=recv_req.dimensions,
|
dimensions=recv_req.dimensions,
|
||||||
lora_id=recv_req.lora_id,
|
lora_id=recv_req.lora_id,
|
||||||
|
|||||||
@@ -1110,6 +1110,8 @@ class SchedulerOutputProcessorMixin:
|
|||||||
):
|
):
|
||||||
req.log_time_stats()
|
req.log_time_stats()
|
||||||
|
|
||||||
|
dp_ranks = [self.dp_rank] * len(rids) if rids else None
|
||||||
|
|
||||||
# Send to detokenizer
|
# Send to detokenizer
|
||||||
if reqs or is_idle_batch:
|
if reqs or is_idle_batch:
|
||||||
if self.model_config.is_multimodal_gen:
|
if self.model_config.is_multimodal_gen:
|
||||||
@@ -1154,6 +1156,7 @@ class SchedulerOutputProcessorMixin:
|
|||||||
placeholder_tokens_val=None,
|
placeholder_tokens_val=None,
|
||||||
retraction_counts=retraction_counts,
|
retraction_counts=retraction_counts,
|
||||||
load=load,
|
load=load,
|
||||||
|
dp_ranks=dp_ranks,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -929,7 +929,8 @@ class TokenizerManager(TokenizerCommunicatorMixin, TokenizerManagerMultiItemMixi
|
|||||||
require_reasoning=obj.require_reasoning,
|
require_reasoning=obj.require_reasoning,
|
||||||
return_hidden_states=obj.return_hidden_states,
|
return_hidden_states=obj.return_hidden_states,
|
||||||
return_routed_experts=obj.return_routed_experts,
|
return_routed_experts=obj.return_routed_experts,
|
||||||
data_parallel_rank=obj.data_parallel_rank,
|
routed_dp_rank=obj.routed_dp_rank,
|
||||||
|
disagg_prefill_dp_rank=obj.disagg_prefill_dp_rank,
|
||||||
priority=obj.priority,
|
priority=obj.priority,
|
||||||
extra_key=obj.extra_key,
|
extra_key=obj.extra_key,
|
||||||
routing_key=obj.routing_key,
|
routing_key=obj.routing_key,
|
||||||
@@ -1518,6 +1519,8 @@ class TokenizerManager(TokenizerCommunicatorMixin, TokenizerManagerMultiItemMixi
|
|||||||
if getattr(recv_obj, "customized_info", None):
|
if getattr(recv_obj, "customized_info", None):
|
||||||
for k, v in recv_obj.customized_info.items():
|
for k, v in recv_obj.customized_info.items():
|
||||||
meta_info[k] = v[i]
|
meta_info[k] = v[i]
|
||||||
|
if getattr(recv_obj, "dp_ranks", None):
|
||||||
|
meta_info["dp_rank"] = recv_obj.dp_ranks[i]
|
||||||
|
|
||||||
if isinstance(recv_obj, BatchStrOutput):
|
if isinstance(recv_obj, BatchStrOutput):
|
||||||
state.text += recv_obj.output_strs[i]
|
state.text += recv_obj.output_strs[i]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ipaddress
|
|||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import urllib
|
import urllib
|
||||||
|
import warnings
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -69,6 +70,10 @@ class MiniLoadBalancer:
|
|||||||
)
|
)
|
||||||
self.enable_trace = False
|
self.enable_trace = False
|
||||||
|
|
||||||
|
self.test_external_dp_routing = router_args.test_external_dp_routing
|
||||||
|
self.prefill_dp_size = None
|
||||||
|
self.decode_dp_size = None
|
||||||
|
|
||||||
def _validate_router_args(self, router_args: RouterArgs):
|
def _validate_router_args(self, router_args: RouterArgs):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"\x1b[33mMiniLB is only for debugging purposes, it only supports random policy!\033[0m"
|
"\x1b[33mMiniLB is only for debugging purposes, it only supports random policy!\033[0m"
|
||||||
@@ -95,6 +100,32 @@ class MiniLoadBalancer:
|
|||||||
trace_set_thread_info("Mini lb")
|
trace_set_thread_info("Mini lb")
|
||||||
uvicorn.run(app, host=self.host, port=self.port)
|
uvicorn.run(app, host=self.host, port=self.port)
|
||||||
|
|
||||||
|
async def _ensure_dp_sizes(self):
|
||||||
|
if self.prefill_dp_size is not None:
|
||||||
|
return
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(f"{self.prefill_urls[0]}/server_info") as resp:
|
||||||
|
info = await resp.json()
|
||||||
|
self.prefill_dp_size = len(info.get("internal_states", [1]))
|
||||||
|
async with session.get(f"{self.decode_urls[0]}/server_info") as resp:
|
||||||
|
info = await resp.json()
|
||||||
|
self.decode_dp_size = len(info.get("internal_states", [1]))
|
||||||
|
logger.info(
|
||||||
|
f"[MiniLB] DP sizes: prefill={self.prefill_dp_size}, decode={self.decode_dp_size}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fork_dp_requests(self, request):
|
||||||
|
p_rank = random.randint(0, self.prefill_dp_size - 1)
|
||||||
|
d_rank = random.randint(0, self.decode_dp_size - 1)
|
||||||
|
|
||||||
|
prefill_req = request.copy()
|
||||||
|
decode_req = request.copy()
|
||||||
|
prefill_req["routed_dp_rank"] = p_rank
|
||||||
|
decode_req["routed_dp_rank"] = d_rank
|
||||||
|
decode_req["disagg_prefill_dp_rank"] = p_rank
|
||||||
|
|
||||||
|
return prefill_req, decode_req, d_rank
|
||||||
|
|
||||||
def select_pair(self):
|
def select_pair(self):
|
||||||
assert len(self.prefill_urls) > 0, "No prefill servers available"
|
assert len(self.prefill_urls) > 0, "No prefill servers available"
|
||||||
assert len(self.decode_urls) > 0, "No decode servers available"
|
assert len(self.decode_urls) > 0, "No decode servers available"
|
||||||
@@ -111,6 +142,16 @@ class MiniLoadBalancer:
|
|||||||
) -> ORJSONResponse:
|
) -> ORJSONResponse:
|
||||||
assert endpoint[0] != "/", f"Endpoint should not start with '/': {endpoint}"
|
assert endpoint[0] != "/", f"Endpoint should not start with '/': {endpoint}"
|
||||||
|
|
||||||
|
expected_decode_dp_rank = None
|
||||||
|
if self.test_external_dp_routing:
|
||||||
|
await self._ensure_dp_sizes()
|
||||||
|
prefill_req, decode_req, expected_decode_dp_rank = self._fork_dp_requests(
|
||||||
|
modified_request
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
prefill_req = modified_request
|
||||||
|
decode_req = modified_request
|
||||||
|
|
||||||
async with aiohttp.ClientSession(
|
async with aiohttp.ClientSession(
|
||||||
timeout=aiohttp.ClientTimeout(
|
timeout=aiohttp.ClientTimeout(
|
||||||
total=self.timeout
|
total=self.timeout
|
||||||
@@ -130,12 +171,12 @@ class MiniLoadBalancer:
|
|||||||
tasks = [
|
tasks = [
|
||||||
session.post(
|
session.post(
|
||||||
f"{prefill_server}/{endpoint}",
|
f"{prefill_server}/{endpoint}",
|
||||||
json=modified_request,
|
json=prefill_req,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
),
|
),
|
||||||
session.post(
|
session.post(
|
||||||
f"{decode_server}/{endpoint}",
|
f"{decode_server}/{endpoint}",
|
||||||
json=modified_request,
|
json=decode_req,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -169,6 +210,16 @@ class MiniLoadBalancer:
|
|||||||
)
|
)
|
||||||
trace_req_finish(bootstrap_room)
|
trace_req_finish(bootstrap_room)
|
||||||
|
|
||||||
|
if expected_decode_dp_rank is not None:
|
||||||
|
actual = ret_json.get("meta_info", {}).get("dp_rank")
|
||||||
|
if actual != expected_decode_dp_rank:
|
||||||
|
return ORJSONResponse(
|
||||||
|
content={
|
||||||
|
"error": f"DP rank mismatch: expected {expected_decode_dp_rank}, got {actual}"
|
||||||
|
},
|
||||||
|
status_code=500,
|
||||||
|
)
|
||||||
|
|
||||||
return ORJSONResponse(
|
return ORJSONResponse(
|
||||||
content=ret_json,
|
content=ret_json,
|
||||||
status_code=decode_response.status,
|
status_code=decode_response.status,
|
||||||
@@ -177,6 +228,10 @@ class MiniLoadBalancer:
|
|||||||
async def generate_stream(
|
async def generate_stream(
|
||||||
self, modified_request, prefill_server, decode_server, endpoint="generate"
|
self, modified_request, prefill_server, decode_server, endpoint="generate"
|
||||||
):
|
):
|
||||||
|
|
||||||
|
if self.test_external_dp_routing:
|
||||||
|
warnings.warn("--test-external-dp-routing is not supported with streaming")
|
||||||
|
|
||||||
assert endpoint[0] != "/", f"Endpoint should not start with '/': {endpoint}"
|
assert endpoint[0] != "/", f"Endpoint should not start with '/': {endpoint}"
|
||||||
|
|
||||||
async def stream_results():
|
async def stream_results():
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class RouterArgs:
|
|||||||
|
|
||||||
# PD-specific configuration
|
# PD-specific configuration
|
||||||
mini_lb: bool = False
|
mini_lb: bool = False
|
||||||
|
test_external_dp_routing: bool = False
|
||||||
pd_disaggregation: bool = False # Enable PD disaggregated mode
|
pd_disaggregation: bool = False # Enable PD disaggregated mode
|
||||||
prefill_urls: List[tuple] = dataclasses.field(
|
prefill_urls: List[tuple] = dataclasses.field(
|
||||||
default_factory=list
|
default_factory=list
|
||||||
@@ -360,6 +361,11 @@ class RouterArgs:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Enable MiniLB",
|
help="Enable MiniLB",
|
||||||
)
|
)
|
||||||
|
pd_group.add_argument(
|
||||||
|
f"--{prefix}test-external-dp-routing",
|
||||||
|
action="store_true",
|
||||||
|
help="(MiniLB only) Randomly assign routed_dp_rank / disagg_prefill_dp_rank per request and verify the response dp_rank matches.",
|
||||||
|
)
|
||||||
pd_group.add_argument(
|
pd_group.add_argument(
|
||||||
f"--{prefix}pd-disaggregation",
|
f"--{prefix}pd-disaggregation",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|||||||
@@ -126,5 +126,43 @@ class TestDisaggregationDPAttentionRoundRobin(TestDisaggregationDPAttention):
|
|||||||
self.assertEqual(result["completed"], 1000)
|
self.assertEqual(result["completed"], 1000)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skip(
|
||||||
|
"Skip this test until new testing logic in mini-lb has been updated in docker image."
|
||||||
|
)
|
||||||
|
class TestDisaggregationDPAttentionExternalRouting(TestDisaggregationDPAttention):
|
||||||
|
"""Test external DP rank assignment via mini-lb --test-external-dp-routing.
|
||||||
|
|
||||||
|
NOTE: In PD disaggregation the response comes from the decode server,
|
||||||
|
so meta_info["dp_rank"] reflects the decode-side DP rank. Prefill DP
|
||||||
|
rank correctness is verified implicitly — if the wrong prefill DP
|
||||||
|
worker were used, KV transfer would fail and the request would error.
|
||||||
|
The mini-lb internally verifies meta_info["dp_rank"] matches the
|
||||||
|
assigned decode dp_rank; a mismatch returns HTTP 500.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def launch_lb(cls):
|
||||||
|
from sglang.test.test_utils import popen_with_error_check
|
||||||
|
|
||||||
|
lb_command = [
|
||||||
|
"python3",
|
||||||
|
"-m",
|
||||||
|
"sglang_router.launch_router",
|
||||||
|
"--pd-disaggregation",
|
||||||
|
"--mini-lb",
|
||||||
|
"--test-external-dp-routing",
|
||||||
|
"--prefill",
|
||||||
|
cls.prefill_url,
|
||||||
|
"--decode",
|
||||||
|
cls.decode_url,
|
||||||
|
"--host",
|
||||||
|
cls.base_host,
|
||||||
|
"--port",
|
||||||
|
cls.lb_port,
|
||||||
|
]
|
||||||
|
cls.process_lb = popen_with_error_check(lb_command)
|
||||||
|
cls.wait_server_ready(cls.lb_url + "/health", process=cls.process_lb)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user