feat: add SGLANG_RAY_BUNDLE_INDICES for fine-grained Ray bundle index control (#24667)

Signed-off-by: Haichuan Hu <kaisennhu@gmail.com>
This commit is contained in:
Haichuan Hu
2026-05-30 02:19:50 -07:00
committed by GitHub
parent 90eb894564
commit acd689b407
7 changed files with 650 additions and 129 deletions
@@ -47,6 +47,62 @@
"Please see [the examples](https://github.com/sgl-project/sglang/tree/main/examples/runtime/engine) for further use cases." "Please see [the examples](https://github.com/sgl-project/sglang/tree/main/examples/runtime/engine) for further use cases."
] ]
}, },
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Ray Integration\n",
"\n",
"When running in a Ray cluster, you can use `RayEngine` with a custom placement group for fine-grained GPU placement control.\n",
"\n",
"### Custom Placement Groups\n",
"\n",
"Pass a `placement_group` with 1-GPU-per-bundle bundles to control exactly which GPUs are used. Each bundle should have exactly 1 GPU for deterministic mapping.\n",
"\n",
"```python\n",
"import ray\n",
"from ray.util.placement_group import placement_group\n",
"from sglang.srt.ray.engine import RayEngine\n",
"\n",
"ray.init()\n",
"\n",
"# Create placement group with specific GPU bundles\n",
"pg = placement_group(\n",
" [{\"GPU\": 1} for _ in range(4)], # 4 bundles, each with 1 GPU\n",
" strategy=\"STRICT_PACK\",\n",
")\n",
"ray.get(pg.ready())\n",
"\n",
"# Launch RayEngine on custom placement group\n",
"engine = RayEngine(\n",
" model_path=\"meta-llama/Meta-Llama-3-8B-Instruct\",\n",
" tp_size=4,\n",
" use_ray=True,\n",
" placement_group=pg,\n",
")\n",
"\n",
"# Optional: specify exact bundle indices via environment variable\n",
"# export SGLANG_RAY_BUNDLE_INDICES=\"0,1,2,3\"\n",
"```\n",
"\n",
"### Bundle Index Control\n",
"\n",
"Use `SGLANG_RAY_BUNDLE_INDICES` environment variable to specify which placement group bundles to use for each worker rank. This enables:\n",
"- Skipping unhealthy GPUs\n",
"- Topology-aware placement (e.g., NVLink-connected GPUs)\n",
"- Non-sequential bundle assignment\n",
"\n",
"```bash\n",
"# Use bundles 0,1,2,7 (skip bundles 3-6) for tp_size=4\n",
"export SGLANG_RAY_BUNDLE_INDICES=\"0,1,2,7\"\n",
"\n",
"# Place workers on NVLink-connected GPUs\n",
"export SGLANG_RAY_BUNDLE_INDICES=\"0,1,2,3\"\n",
"```\n",
"\n",
"The number of indices must match `world_size` (`tp_size * pp_size * dp_size`, or `tp_size * pp_size` when `enable_dp_attention=True`)."
]
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
@@ -629,6 +629,11 @@ SGLang supports various environment variables that can be used to configure its
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Set one visible device per process for distributed computing</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Set one visible device per process for distributed computing</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}>`false`</td> <td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}>`false`</td>
</tr> </tr>
<tr>
<td style={{padding: "9px 12px", fontWeight: 500, backgroundColor: "rgba(255,255,255,0.02)"}}><code>SGLANG_RAY_BUNDLE_INDICES</code></td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.05)"}}>Comma-separated bundle indices for Ray actor placement (e.g., <code>"0,1,2,3"</code>). Must match world_size. Enables fine-grained GPU assignment in custom placement groups.</td>
<td style={{padding: "9px 12px", backgroundColor: "rgba(255,255,255,0.02)"}}>Not set</td>
</tr>
</tbody> </tbody>
</table> </table>
+2
View File
@@ -323,6 +323,8 @@ class Envs:
# Model Parallel # Model Parallel
SGLANG_USE_MESSAGE_QUEUE_BROADCASTER = EnvBool(True) SGLANG_USE_MESSAGE_QUEUE_BROADCASTER = EnvBool(True)
SGLANG_ONE_VISIBLE_DEVICE_PER_PROCESS = EnvBool(False) SGLANG_ONE_VISIBLE_DEVICE_PER_PROCESS = EnvBool(False)
# Comma-separated bundle indices for Ray Custom PG mode (e.g., "0,1,2,7").
SGLANG_RAY_BUNDLE_INDICES = EnvStr("")
# Override the distributed init method used by torch.distributed.init_process_group. # Override the distributed init method used by torch.distributed.init_process_group.
# Set to "env://" to use an externally-created TCPStore via MASTER_ADDR/MASTER_PORT. # Set to "env://" to use an externally-created TCPStore via MASTER_ADDR/MASTER_PORT.
SGLANG_DISTRIBUTED_INIT_METHOD_OVERRIDE = EnvStr(None) SGLANG_DISTRIBUTED_INIT_METHOD_OVERRIDE = EnvStr(None)
+118 -75
View File
@@ -20,15 +20,16 @@ from typing import List, Optional
import ray import ray
import zmq import zmq
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
from sglang.srt.entrypoints.engine import ( from sglang.srt.entrypoints.engine import _calculate_rank_ranges
_calculate_rank_ranges,
_compute_parallelism_ranks,
)
from sglang.srt.layers.dp_attention import compute_dp_attention_world_info from sglang.srt.layers.dp_attention import compute_dp_attention_world_info
from sglang.srt.managers.data_parallel_controller import DataParallelController from sglang.srt.managers.data_parallel_controller import DataParallelController
from sglang.srt.ray.scheduler_actor import SchedulerActor from sglang.srt.ray.engine import (
_compute_world_size,
_create_scheduler_actor,
_get_bundle_node_ip,
_resolve_bundle_indices,
)
from sglang.srt.server_args import PortArgs, ServerArgs from sglang.srt.server_args import PortArgs, ServerArgs
from sglang.srt.utils.network import bind_port, get_zmq_socket, get_zmq_socket_on_host from sglang.srt.utils.network import bind_port, get_zmq_socket, get_zmq_socket_on_host
@@ -48,7 +49,7 @@ class RayDataParallelController(DataParallelController):
server_args: ServerArgs, server_args: ServerArgs,
port_args: PortArgs, port_args: PortArgs,
placement_group, placement_group,
bundle_for_node: List[int], bundle_for_node: Optional[List[int]],
rank0_node_ip: str, rank0_node_ip: str,
): ):
# Set Ray-specific attributes BEFORE super().__init__() because the # Set Ray-specific attributes BEFORE super().__init__() because the
@@ -127,87 +128,129 @@ class RayDataParallelController(DataParallelController):
): ):
"""Create SchedulerActor Ray actors for one TP group (one DP rank). """Create SchedulerActor Ray actors for one TP group (one DP rank).
For DP attention, dp_rank=None and worker_ports is provided; the dp_rank Args:
is derived from tp_rank via compute_dp_attention_world_info. dp_rank: DP rank for regular DP; None for DP attention (derived from tp_rank).
worker_ports: Pre-allocated ports for DP attention; None for regular DP.
For regular DP, dp_rank is an integer and worker_ports is None.
""" """
nnodes = server_args.nnodes nnodes = server_args.nnodes
batch_start_idx = len(self.scheduler_actors) batch_start_idx = len(self.scheduler_actors)
for node_idx in range(nnodes): if self.server_args.placement_group is None:
bundle_idx = self.bundle_for_node[node_idx] for node_idx in range(nnodes):
pp_range, tp_range, pp_per_node, tp_per_node = _calculate_rank_ranges( bundle_idx = self.bundle_for_node[node_idx]
nnodes, server_args.pp_size, server_args.tp_size, node_rank=node_idx pp_range, tp_range, pp_per_node, tp_per_node = _calculate_rank_ranges(
) nnodes, server_args.pp_size, server_args.tp_size, node_rank=node_idx
)
for pp_rank in pp_range:
for tp_rank in tp_range:
rank_port_args = port_args
actual_dp_rank = dp_rank
for pp_rank in pp_range: local_gpu_idx = (pp_rank % pp_per_node) * tp_per_node + (
for tp_rank in tp_range: tp_rank % tp_per_node
rank_port_args = port_args
actual_dp_rank = dp_rank
if server_args.enable_dp_attention:
# DP attention: derive dp_rank from tp_rank
_, _, actual_dp_rank, _ = compute_dp_attention_world_info(
server_args.enable_dp_attention,
tp_rank,
server_args.tp_size,
server_args.dp_size,
server_args.attn_cp_size,
) )
rank_port_args = PortArgs.init_new(
server_args, actual_dp_rank, worker_ports if server_args.enable_dp_attention:
_, _, actual_dp_rank, _ = compute_dp_attention_world_info(
server_args.enable_dp_attention,
tp_rank,
server_args.tp_size,
server_args.dp_size,
server_args.attn_cp_size,
)
rank_port_args = PortArgs.init_new(
server_args, actual_dp_rank, worker_ports
)
# All DP ranks share the same NCCL port (reuse TP group)
rank_port_args.nccl_port = port_args.nccl_port
rank_port_args.instance_id = port_args.instance_id
# The detokenizer and tokenizer bind using the
# original port_args addresses (127.0.0.1 when
# dist_init_addr is unset). Scheduler actors must
# connect to the same addresses.
rank_port_args.detokenizer_ipc_name = (
port_args.detokenizer_ipc_name
)
rank_port_args.tokenizer_ipc_name = (
port_args.tokenizer_ipc_name
)
dist_init_addr = (
f"{self.rank0_node_ip}:{rank_port_args.nccl_port}"
) )
# All DP ranks share the same NCCL port (reuse TP group)
rank_port_args.nccl_port = port_args.nccl_port actor = _create_scheduler_actor(
rank_port_args.instance_id = port_args.instance_id pg=self.pg,
# The detokenizer and tokenizer bind using the bundle_idx=bundle_idx,
# original port_args addresses (127.0.0.1 when gpu_id=local_gpu_idx,
# dist_init_addr is unset). Scheduler actors must server_args=server_args,
# connect to the same addresses. port_args=rank_port_args,
rank_port_args.detokenizer_ipc_name = ( tp_rank=tp_rank,
port_args.detokenizer_ipc_name pp_rank=pp_rank,
dp_rank=actual_dp_rank,
dist_init_addr=dist_init_addr,
rank0_node_ip=self.rank0_node_ip,
) )
rank_port_args.tokenizer_ipc_name = port_args.tokenizer_ipc_name self.scheduler_actors.append(actor)
local_gpu_idx = (pp_rank % pp_per_node) * tp_per_node + ( else:
tp_rank % tp_per_node world_size = _compute_world_size(server_args)
bundle_indices = _resolve_bundle_indices(self.pg, world_size)
ranks_per_tp_group = server_args.tp_size * server_args.pp_size
if dp_rank is not None:
start_rank = dp_rank * ranks_per_tp_group
end_rank = start_rank + ranks_per_tp_group
# Each DP group must use its own local rank-0's node IP for
# NCCL rendezvous, not the world rank-0's node IP.
local_rank0_bundle_idx = bundle_indices[start_rank]
local_rank0_node_ip = _get_bundle_node_ip(
self.pg, local_rank0_bundle_idx
)
else:
start_rank = 0
end_rank = world_size
local_rank0_node_ip = self.rank0_node_ip
for global_rank in range(start_rank, end_rank):
local_rank = global_rank % ranks_per_tp_group
pp_rank = local_rank // server_args.tp_size
tp_rank = local_rank % server_args.tp_size
rank_port_args = port_args
actual_dp_rank = dp_rank
bundle_idx = bundle_indices[global_rank]
if server_args.enable_dp_attention:
_, _, actual_dp_rank, _ = compute_dp_attention_world_info(
server_args.enable_dp_attention,
tp_rank,
server_args.tp_size,
server_args.dp_size,
server_args.attn_cp_size,
) )
rank_port_args = PortArgs.init_new(
attn_cp_rank, moe_dp_rank, moe_ep_rank = _compute_parallelism_ranks( server_args, actual_dp_rank, worker_ports
server_args, tp_rank
) )
rank_port_args.nccl_port = port_args.nccl_port
rank_port_args.detokenizer_ipc_name = port_args.detokenizer_ipc_name
rank_port_args.tokenizer_ipc_name = port_args.tokenizer_ipc_name
# Each DP group needs a unique dist_init_addr for its own dist_init_addr = f"{local_rank0_node_ip}:{rank_port_args.nccl_port}"
# torch.distributed process group. Use nccl_port which is
# unique per DP group (regular DP) or shared (DP attention).
dist_init_addr = f"{self.rank0_node_ip}:{rank_port_args.nccl_port}"
actor = SchedulerActor.options( actor = _create_scheduler_actor(
num_cpus=0, pg=self.pg,
num_gpus=1, bundle_idx=bundle_idx,
name=( gpu_id=0, # Each bundle has exactly 1 GPU
f"sglang_scheduler_node{self.rank0_node_ip}" server_args=server_args,
f"_dp{actual_dp_rank}_pp{pp_rank}_tp{tp_rank}" port_args=rank_port_args,
f"_pg{self.pg.id.hex()[:8]}_bundle{bundle_idx}" tp_rank=tp_rank,
), pp_rank=pp_rank,
scheduling_strategy=PlacementGroupSchedulingStrategy( dp_rank=actual_dp_rank,
placement_group=self.pg, dist_init_addr=dist_init_addr,
placement_group_bundle_index=bundle_idx, rank0_node_ip=local_rank0_node_ip,
), )
).remote( self.scheduler_actors.append(actor)
server_args=server_args,
port_args=rank_port_args,
gpu_id=local_gpu_idx,
tp_rank=tp_rank,
attn_cp_rank=attn_cp_rank,
moe_dp_rank=moe_dp_rank,
moe_ep_rank=moe_ep_rank,
pp_rank=pp_rank,
dp_rank=actual_dp_rank,
dist_init_addr=dist_init_addr,
)
self.scheduler_actors.append(actor)
# Wait for all actors created in this call to initialize # Wait for all actors created in this call to initialize
batch_actors = self.scheduler_actors[batch_start_idx:] batch_actors = self.scheduler_actors[batch_start_idx:]
+252 -54
View File
@@ -18,7 +18,7 @@ from __future__ import annotations
import dataclasses import dataclasses
import logging import logging
import threading import threading
from typing import Callable from typing import Callable, List, Optional
import ray import ray
from ray.util.placement_group import PlacementGroup from ray.util.placement_group import PlacementGroup
@@ -30,6 +30,7 @@ from sglang.srt.entrypoints.engine import (
_calculate_rank_ranges, _calculate_rank_ranges,
_compute_parallelism_ranks, _compute_parallelism_ranks,
) )
from sglang.srt.environ import envs
from sglang.srt.ray.scheduler_actor import SchedulerActor from sglang.srt.ray.scheduler_actor import SchedulerActor
from sglang.srt.server_args import PortArgs, ServerArgs from sglang.srt.server_args import PortArgs, ServerArgs
@@ -76,9 +77,166 @@ def _find_engine_bundle(
) )
def _get_bundle_node_ip(placement_group: PlacementGroup, bundle_idx: int) -> str:
"""Get the IP address of the node where a specific bundle is located.
Args:
placement_group: The placement group
bundle_idx: Bundle index to query
Returns:
IP address of the node where the bundle is located.
"""
@ray.remote(num_cpus=0, num_gpus=0)
def get_node_ip():
return ray.util.get_node_ip_address()
return ray.get(
get_node_ip.options(
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=placement_group,
placement_group_bundle_index=bundle_idx,
),
).remote()
)
def _compute_world_size(server_args: ServerArgs) -> int:
"""Compute world_size (total number of scheduler actors/GPUs needed).
Normal: dp_size * tp_size * pp_size; DP attention: tp_size * pp_size.
"""
if server_args.enable_dp_attention:
return server_args.tp_size * server_args.pp_size
return server_args.dp_size * server_args.tp_size * server_args.pp_size
def _resolve_bundle_indices(pg: PlacementGroup, world_size: int) -> List[int]:
"""Resolve bundle indices for Custom PG mode.
Parses SGLANG_RAY_BUNDLE_INDICES env var if set; otherwise returns
sequential indices [0, 1, ..., world_size-1].
Args:
pg: Placement group (used to get total_bundles count).
world_size: Number of bundle indices expected (pre-computed via _compute_world_size).
Returns:
List of bundle indices of length world_size.
"""
total_bundles = len(pg.bundle_specs)
indices_str = envs.SGLANG_RAY_BUNDLE_INDICES.get()
if not indices_str:
return list(range(world_size))
indices = list(map(int, indices_str.split(",")))
if len(indices) != world_size:
raise ValueError(
f"SGLANG_RAY_BUNDLE_INDICES has {len(indices)} values, "
f"expected {world_size}"
)
if len(set(indices)) != len(indices):
raise ValueError(f"SGLANG_RAY_BUNDLE_INDICES has duplicates: {indices}")
for idx in indices:
if idx < 0 or idx >= total_bundles:
raise ValueError(f"Bundle index {idx} out of range [0, {total_bundles})")
return indices
def _validate_custom_placement_group(pg: PlacementGroup, world_size: int) -> None:
"""Validate custom placement group: 1 GPU per bundle, enough GPU bundles for world_size.
Args:
pg: User-provided placement group.
world_size: Number of GPU bundles required.
"""
bundles = pg.bundle_specs
gpu_bundle_count = 0
for bundle in bundles:
gpu_count = bundle.get("GPU", 0)
if gpu_count > 1:
raise ValueError(
"Custom placement group must have exactly 1 GPU per bundle. "
f"Found bundle with {gpu_count} GPUs."
)
if gpu_count > 0:
gpu_bundle_count += 1
if gpu_bundle_count < world_size:
raise ValueError(
f"Custom placement group has {gpu_bundle_count} GPU bundles, "
f"but needs {world_size} for world_size. "
"Provide more bundles or reduce parallelism."
)
def _create_scheduler_actor(
pg: PlacementGroup,
bundle_idx: int,
gpu_id: int,
server_args: ServerArgs,
port_args: PortArgs,
tp_rank: int,
pp_rank: int,
dp_rank: int,
dist_init_addr: str,
rank0_node_ip: str,
) -> SchedulerActor:
"""Create a SchedulerActor on the given placement group bundle.
Args:
pg: Placement group to schedule actor onto.
bundle_idx: Bundle index within the placement group.
gpu_id: GPU ID within the bundle (0 for custom PG, computed for auto PG).
rank0_node_ip: IP of rank-0's node, used for NCCL rendezvous.
dist_init_addr: Distributed init address (tcp://rank0_node_ip:nccl_port).
"""
attn_cp_rank, moe_dp_rank, moe_ep_rank = _compute_parallelism_ranks(
server_args, tp_rank
)
return SchedulerActor.options(
num_cpus=0,
num_gpus=1,
name=(
f"sglang_scheduler_node{rank0_node_ip}"
f"_dp{dp_rank}_pp{pp_rank}_tp{tp_rank}"
f"_pg{pg.id.hex()[:8]}_bundle{bundle_idx}"
),
scheduling_strategy=PlacementGroupSchedulingStrategy(
placement_group=pg,
placement_group_bundle_index=bundle_idx,
),
).remote(
server_args=server_args,
port_args=port_args,
gpu_id=gpu_id,
tp_rank=tp_rank,
attn_cp_rank=attn_cp_rank,
moe_dp_rank=moe_dp_rank,
moe_ep_rank=moe_ep_rank,
pp_rank=pp_rank,
dp_rank=dp_rank,
dist_init_addr=dist_init_addr,
)
class RayEngine(Engine): class RayEngine(Engine):
"""Engine using Ray actors for scheduler processes.""" """Engine using Ray actors for scheduler processes."""
def __init__(self, **kwargs):
placement_group = kwargs.pop("placement_group", None)
if "log_level" not in kwargs:
kwargs["log_level"] = "error"
server_args = ServerArgs(**kwargs)
server_args.placement_group = placement_group
super().__init__(server_args=server_args)
def shutdown(self): def shutdown(self):
"""Shutdown the engine — kill Ray scheduler actors then local processes.""" """Shutdown the engine — kill Ray scheduler actors then local processes."""
for actor in self._scheduler_init_result.scheduler_actors: for actor in self._scheduler_init_result.scheduler_actors:
@@ -101,7 +259,7 @@ class RayEngine(Engine):
Tuple of (RaySchedulerInitResult, None). Tuple of (RaySchedulerInitResult, None).
scheduler_procs is None since Ray uses actors instead of mp.Process. scheduler_procs is None since Ray uses actors instead of mp.Process.
""" """
pg = ray.util.get_current_placement_group() pg = server_args.placement_group or ray.util.get_current_placement_group()
if pg is None: if pg is None:
from ray.util.placement_group import ( from ray.util.placement_group import (
placement_group as create_placement_group, placement_group as create_placement_group,
@@ -130,69 +288,102 @@ class RayEngine(Engine):
) )
ray.get(pg.ready()) ray.get(pg.ready())
is_custom_pg = server_args.placement_group is not None
nnodes = server_args.nnodes nnodes = server_args.nnodes
world_size = _compute_world_size(server_args)
# co-located with the Engine and rank0 scheduler at the same node if not is_custom_pg:
engine_bundle, engine_ip = _find_engine_bundle(pg, nnodes) engine_bundle, engine_ip = _find_engine_bundle(pg, nnodes)
bundle_for_node = [engine_bundle] + [ bundle_for_node = [engine_bundle] + [
i for i in range(nnodes) if i != engine_bundle i for i in range(nnodes) if i != engine_bundle
] ]
rank0_node_ip = engine_ip rank0_node_ip = engine_ip
else:
try:
_validate_custom_placement_group(pg, world_size)
except ValueError as e:
logger.error(f"Custom placement group validation failed: {e}")
raise RuntimeError(
f"Custom placement group validation failed: {e}"
) from e
bundle_for_node = None
indices_str = envs.SGLANG_RAY_BUNDLE_INDICES.get()
rank0_bundle_idx = int(indices_str.split(",")[0]) if indices_str else 0
rank0_node_ip = _get_bundle_node_ip(pg, rank0_bundle_idx)
if server_args.dp_size == 1: if server_args.dp_size == 1:
# Launch tensor parallel scheduler actors
world_size = server_args.tp_size * server_args.pp_size
gpus_per_node = world_size // nnodes
logger.info(
f"Ray cluster: {nnodes} nodes, "
f"Use {gpus_per_node} GPUs/node, world_size={world_size}"
)
dist_init_addr = f"{rank0_node_ip}:{port_args.nccl_port}" dist_init_addr = f"{rank0_node_ip}:{port_args.nccl_port}"
logger.info(f"dist_init_addr: {dist_init_addr}") logger.info(f"dist_init_addr: {dist_init_addr}")
scheduler_actors = [] scheduler_actors = []
for node_idx in range(nnodes): if not is_custom_pg:
bundle_idx = bundle_for_node[node_idx] gpus_per_node = world_size // nnodes
pp_range, tp_range, pp_per_node, tp_per_node = _calculate_rank_ranges( logger.info(
nnodes, f"Ray cluster (auto PG): {nnodes} nodes, "
server_args.pp_size, f"{gpus_per_node} GPUs/node, world_size={world_size}"
server_args.tp_size,
node_rank=node_idx,
) )
for pp_rank in pp_range:
for tp_rank in tp_range:
local_gpu_idx = (pp_rank % pp_per_node) * tp_per_node + (
tp_rank % tp_per_node
)
attn_cp_rank, moe_dp_rank, moe_ep_rank = ( for node_idx in range(nnodes):
_compute_parallelism_ranks(server_args, tp_rank) bundle_idx = bundle_for_node[node_idx]
pp_range, tp_range, pp_per_node, tp_per_node = (
_calculate_rank_ranges(
nnodes,
server_args.pp_size,
server_args.tp_size,
node_rank=node_idx,
) )
)
for pp_rank in pp_range:
for tp_rank in tp_range:
local_gpu_idx = (pp_rank % pp_per_node) * tp_per_node + (
tp_rank % tp_per_node
)
actor = SchedulerActor.options( actor = _create_scheduler_actor(
num_cpus=0, pg=pg,
num_gpus=1, bundle_idx=bundle_idx,
name=f"sglang_scheduler_node{rank0_node_ip}_pp{pp_rank}_tp{tp_rank}_pg{pg.id.hex()[:8]}_bundle{bundle_idx}", gpu_id=local_gpu_idx,
scheduling_strategy=PlacementGroupSchedulingStrategy( server_args=server_args,
placement_group=pg, port_args=port_args,
placement_group_bundle_index=bundle_idx, tp_rank=tp_rank,
), pp_rank=pp_rank,
).remote( dp_rank=0,
server_args=server_args, dist_init_addr=dist_init_addr,
port_args=port_args, rank0_node_ip=rank0_node_ip,
gpu_id=local_gpu_idx, )
tp_rank=tp_rank, scheduler_actors.append(actor)
attn_cp_rank=attn_cp_rank,
moe_dp_rank=moe_dp_rank, else:
moe_ep_rank=moe_ep_rank, try:
pp_rank=pp_rank, bundle_indices = _resolve_bundle_indices(pg, world_size)
dp_rank=0, except ValueError as e:
dist_init_addr=dist_init_addr, logger.error(f"Failed to resolve bundle indices: {e}")
) raise RuntimeError(f"Failed to resolve bundle indices: {e}") from e
scheduler_actors.append(actor)
logger.info(
f"Ray cluster (custom PG): world_size={world_size}, "
f"bundle_indices={bundle_indices}"
)
for rank in range(world_size):
pp_rank = rank // server_args.tp_size
tp_rank = rank % server_args.tp_size
bundle_idx = bundle_indices[rank]
actor = _create_scheduler_actor(
pg=pg,
bundle_idx=bundle_idx,
gpu_id=0, # Each bundle has exactly 1 GPU
server_args=server_args,
port_args=port_args,
tp_rank=tp_rank,
pp_rank=pp_rank,
dp_rank=0,
dist_init_addr=dist_init_addr,
rank0_node_ip=rank0_node_ip,
)
scheduler_actors.append(actor)
try: try:
scheduler_infos = ray.get( scheduler_infos = ray.get(
@@ -228,7 +419,11 @@ class RayEngine(Engine):
# Launch the data parallel controller # Launch the data parallel controller
return ( return (
cls._launch_dp_scheduler_processes( cls._launch_dp_scheduler_processes(
server_args, port_args, pg, bundle_for_node, rank0_node_ip server_args,
port_args,
pg,
bundle_for_node,
rank0_node_ip,
), ),
None, None,
) )
@@ -239,7 +434,7 @@ class RayEngine(Engine):
server_args: ServerArgs, server_args: ServerArgs,
port_args: PortArgs, port_args: PortArgs,
pg, pg,
bundle_for_node: list, bundle_for_node: Optional[List[int]],
rank0_node_ip: str, rank0_node_ip: str,
) -> RaySchedulerInitResult: ) -> RaySchedulerInitResult:
"""Launch DP schedulers via RayDataParallelController.""" """Launch DP schedulers via RayDataParallelController."""
@@ -266,6 +461,9 @@ class RayEngine(Engine):
server_args, server_args,
dist_init_addr=f"{rank0_node_ip}:{port_args.nccl_port}", dist_init_addr=f"{rank0_node_ip}:{port_args.nccl_port}",
) )
# dataclasses.replace only copies declared fields; placement_group is
# a dynamic attribute that must be manually appended after the rebuild.
dp_server_args.placement_group = server_args.placement_group
# Create the DP controller in-process. This blocks until all actors # Create the DP controller in-process. This blocks until all actors
# are initialized and their event loops have started. # are initialized and their event loops have started.
+2
View File
@@ -44,6 +44,8 @@ def launch_server(
if execute_warmup_func is None: if execute_warmup_func is None:
execute_warmup_func = _execute_server_warmup execute_warmup_func = _execute_server_warmup
server_args.placement_group = None
( (
tokenizer_manager, tokenizer_manager,
template_manager, template_manager,
+215
View File
@@ -3,6 +3,7 @@
Tests the Ray actor scheduler backend: Tests the Ray actor scheduler backend:
- Offline inference via Engine(use_ray=True) inside a Ray actor on a placement group - Offline inference via Engine(use_ray=True) inside a Ray actor on a placement group
- Data parallel (DP) and DP attention support - Data parallel (DP) and DP attention support
- Custom placement_group and SGLANG_RAY_BUNDLE_INDICES for fine-grained bundle control
- Error paths in RayEngine._launch_scheduler_processes() - Error paths in RayEngine._launch_scheduler_processes()
- HTTP server launched via --use-ray flag - HTTP server launched via --use-ray flag
@@ -11,12 +12,14 @@ Usage:
python -m pytest test/manual/test_ray_engine.py::TestRayEngineOfflineTP1 -v -s python -m pytest test/manual/test_ray_engine.py::TestRayEngineOfflineTP1 -v -s
python -m pytest test/manual/test_ray_engine.py::TestRayEngineErrors -v -s python -m pytest test/manual/test_ray_engine.py::TestRayEngineErrors -v -s
python -m pytest test/manual/test_ray_engine.py::TestRayHTTPServerTP1 -v -s python -m pytest test/manual/test_ray_engine.py::TestRayHTTPServerTP1 -v -s
python -m pytest test/manual/test_ray_engine.py::TestRayEnginePlacementGroupErrors -v -s
# 2-GPU tests # 2-GPU tests
python -m pytest test/manual/test_ray_engine.py::TestRayEngineOfflineTP2 -v -s python -m pytest test/manual/test_ray_engine.py::TestRayEngineOfflineTP2 -v -s
python -m pytest test/manual/test_ray_engine.py::TestRayEngineOfflinePP2 -v -s python -m pytest test/manual/test_ray_engine.py::TestRayEngineOfflinePP2 -v -s
python -m pytest test/manual/test_ray_engine.py::TestRayEngineOfflineDP2 -v -s python -m pytest test/manual/test_ray_engine.py::TestRayEngineOfflineDP2 -v -s
python -m pytest test/manual/test_ray_engine.py::TestRayEngineOfflineDPAttention -v -s python -m pytest test/manual/test_ray_engine.py::TestRayEngineOfflineDPAttention -v -s
python -m pytest test/manual/test_ray_engine.py::TestRayEnginePlacementGroup -v -s
""" """
from __future__ import annotations from __future__ import annotations
@@ -516,5 +519,217 @@ class TestRayHTTPServerTP1(unittest.TestCase):
self.assertGreater(len(data["text"]), 0, f"Empty output for: {prompt}") self.assertGreater(len(data["text"]), 0, f"Empty output for: {prompt}")
# ---------------------------------------------------------------------------
# Tests: Custom placement_group and SGLANG_RAY_BUNDLE_INDICES
# ---------------------------------------------------------------------------
@unittest.skipUnless(_has_ray, "ray is not installed")
@unittest.skipUnless(_NUM_GPUS >= 2, "requires at least 2 GPUs")
class TestRayEnginePlacementGroup(unittest.TestCase):
"""Test RayEngine with custom placement_group and SGLANG_RAY_BUNDLE_INDICES."""
@classmethod
def setUpClass(cls):
if not ray.is_initialized():
ray.init(log_to_driver=True, runtime_env=_RAY_RUNTIME_ENV)
@classmethod
def tearDownClass(cls):
ray.shutdown()
def test_custom_pg_dp1_tp2(self):
"""Test custom placement_group with dp_size=1, tp_size=2."""
from sglang.srt.ray.engine import RayEngine
pg = placement_group([{"GPU": 1}] * 2, strategy="STRICT_PACK")
ray.get(pg.ready())
engine = RayEngine(
model_path=_MODEL,
tp_size=2,
placement_group=pg,
use_ray=True,
)
result = engine.generate("The capital of France is", _SAMPLING_PARAMS)
self.assertIn("text", result)
self.assertGreater(len(result["text"]), 0)
print(f"Generated (dp=1, tp=2, custom PG): {result['text'][:200]}")
engine.shutdown()
ray.util.remove_placement_group(pg)
def test_bundle_indices_dp1_tp2(self):
"""Test SGLANG_RAY_BUNDLE_INDICES with dp_size=1, tp_size=2."""
from sglang.srt.ray.engine import RayEngine
os.environ["SGLANG_RAY_BUNDLE_INDICES"] = "0,1"
pg = placement_group([{"GPU": 1}] * 2, strategy="STRICT_PACK")
ray.get(pg.ready())
engine = RayEngine(
model_path=_MODEL,
tp_size=2,
placement_group=pg,
use_ray=True,
)
result = engine.generate("The capital of France is", _SAMPLING_PARAMS)
self.assertIn("text", result)
self.assertGreater(len(result["text"]), 0)
print(f"Generated (dp=1, tp=2, indices=0,1): {result['text'][:200]}")
engine.shutdown()
ray.util.remove_placement_group(pg)
del os.environ["SGLANG_RAY_BUNDLE_INDICES"]
def test_custom_pg_dp2_tp1(self):
"""Test custom placement_group with dp_size=2, tp_size=1."""
from sglang.srt.ray.engine import RayEngine
pg = placement_group([{"GPU": 1}] * 2, strategy="STRICT_PACK")
ray.get(pg.ready())
engine = RayEngine(
model_path=_MODEL,
tp_size=1,
dp_size=2,
placement_group=pg,
use_ray=True,
)
result = engine.generate("The capital of France is", _SAMPLING_PARAMS)
self.assertIn("text", result)
self.assertGreater(len(result["text"]), 0)
print(f"Generated (dp=2, tp=1, custom PG): {result['text'][:200]}")
engine.shutdown()
ray.util.remove_placement_group(pg)
def test_bundle_indices_skip_bundle(self):
"""Test skipping unhealthy GPU by using bundle_indices."""
from sglang.srt.ray.engine import RayEngine
os.environ["SGLANG_RAY_BUNDLE_INDICES"] = "1" # Skip bundle 0
pg = placement_group([{"GPU": 1}] * 2, strategy="STRICT_PACK")
ray.get(pg.ready())
engine = RayEngine(
model_path=_MODEL,
tp_size=1,
placement_group=pg,
use_ray=True,
)
result = engine.generate("The capital of France is", _SAMPLING_PARAMS)
self.assertIn("text", result)
self.assertGreater(len(result["text"]), 0)
print(f"Generated (tp=1, skip bundle 0): {result['text'][:200]}")
engine.shutdown()
ray.util.remove_placement_group(pg)
del os.environ["SGLANG_RAY_BUNDLE_INDICES"]
def test_custom_pg_dp_attention(self):
"""Test custom placement_group with enable_dp_attention=True."""
from sglang.srt.ray.engine import RayEngine
pg = placement_group([{"GPU": 1}] * 2, strategy="STRICT_PACK")
ray.get(pg.ready())
engine = RayEngine(
model_path=_DP_ATTN_MODEL,
tp_size=2,
placement_group=pg,
use_ray=True,
enable_dp_attention=True,
)
result = engine.generate("The capital of France is", _SAMPLING_PARAMS)
self.assertIn("text", result)
self.assertGreater(len(result["text"]), 0)
print(f"Generated (dp attention, tp=2, custom PG): {result['text'][:200]}")
engine.shutdown()
ray.util.remove_placement_group(pg)
@unittest.skipUnless(_has_ray, "ray is not installed")
@unittest.skipUnless(_NUM_GPUS >= 1, "requires at least 1 GPU")
class TestRayEnginePlacementGroupErrors(unittest.TestCase):
"""Test error handling for placement_group and bundle indices."""
@classmethod
def setUpClass(cls):
if not ray.is_initialized():
ray.init(log_to_driver=True, runtime_env=_RAY_RUNTIME_ENV)
@classmethod
def tearDownClass(cls):
ray.shutdown()
def test_multi_gpu_bundle_raises_error(self):
"""Custom PG with multi-GPU bundles should raise an error."""
@ray.remote(num_gpus=0)
def _try_multi_gpu_bundle():
pg = placement_group([{"GPU": 2}], strategy="STRICT_PACK")
ray.get(pg.ready())
from sglang.srt.ray.engine import RayEngine
try:
RayEngine(
model_path=_MODEL,
tp_size=2,
placement_group=pg,
use_ray=True,
)
return None
except Exception as e:
return str(e)
finally:
ray.util.remove_placement_group(pg)
error_msg = ray.get(_try_multi_gpu_bundle.remote(), timeout=120)
self.assertIsNotNone(error_msg)
self.assertIn("exactly 1 GPU per bundle", error_msg)
def test_invalid_bundle_index_raises_error(self):
"""SGLANG_RAY_BUNDLE_INDICES with invalid index should raise an error."""
@ray.remote(num_gpus=0)
def _try_invalid_bundle_index():
import os
os.environ["SGLANG_RAY_BUNDLE_INDICES"] = "0,10"
pg = placement_group([{"GPU": 1}] * 2, strategy="STRICT_PACK")
ray.get(pg.ready())
from sglang.srt.ray.engine import RayEngine
try:
RayEngine(
model_path=_MODEL,
tp_size=2,
placement_group=pg,
use_ray=True,
)
return None
except Exception as e:
return str(e)
finally:
os.environ.pop("SGLANG_RAY_BUNDLE_INDICES", None)
ray.util.remove_placement_group(pg)
error_msg = ray.get(_try_invalid_bundle_index.remote(), timeout=120)
self.assertIsNotNone(error_msg)
self.assertIn("out of range", error_msg)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()