-
Notifications
You must be signed in to change notification settings - Fork 360
RDT weight sync: GPU->GPU zero copy transfer through SGLang Ray actor backend #1313
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
base: main
Are you sure you want to change the base?
Changes from 9 commits
c414b94
5eb5756
b74ab7f
4c5a351
f8bc705
f961917
4f5d2d7
96aa1ee
bcd5cbf
0150527
14fd82f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,9 +6,12 @@ | |
| import time | ||
| from urllib.parse import quote | ||
|
|
||
| import ray | ||
| import requests | ||
| import sglang_router | ||
| from packaging.version import parse | ||
| from ray.util.placement_group import PlacementGroup | ||
| from sglang.srt.environ import envs | ||
| from sglang.srt.server_args import ServerArgs | ||
| from sglang.srt.utils import kill_process_tree | ||
| from urllib3.exceptions import NewConnectionError | ||
|
|
@@ -68,7 +71,12 @@ def _get_gpu_uuids(gpu_ids: list[int]) -> list[str | None]: | |
|
|
||
|
|
||
| def launch_server_process(server_args: ServerArgs) -> multiprocessing.Process: | ||
| from sglang.srt.entrypoints.http_server import launch_server | ||
| if server_args.use_ray: | ||
| # entrypoints.http_server ignores use_ray and starts mp.Process schedulers, | ||
| # which have no SchedulerActor for RDT to pull from. | ||
| from sglang.srt.ray.http_server import launch_server | ||
| else: | ||
| from sglang.srt.entrypoints.http_server import launch_server | ||
|
|
||
| multiprocessing.set_start_method("spawn", force=True) | ||
| server_args.host = server_args.host.strip("[]") | ||
|
|
@@ -132,13 +140,18 @@ def __init__( | |
| base_gpu_id: int | None = None, | ||
| sglang_overrides: dict | None = None, | ||
| num_gpus_per_engine: int | None = None, | ||
| placement_group: PlacementGroup | None = None, | ||
| pg_bundles: list[int] | None = None, | ||
| ): | ||
| self.args = args | ||
| self.rank = rank | ||
| self.worker_type = worker_type | ||
| self.base_gpu_id = base_gpu_id | ||
| self.sglang_overrides = sglang_overrides or {} | ||
| self.num_gpus_per_engine = num_gpus_per_engine | ||
| self.placement_group = placement_group | ||
| self.pg_bundles = pg_bundles | ||
| self._scheduler_actors = [] | ||
|
|
||
| def get_topology_info(self) -> dict: | ||
| """Placement facts for the dashboard timeline. ``base_gpu_id`` is | ||
|
|
@@ -244,8 +257,30 @@ def _sanity_check_server_args(actual_server_args, expect_server_args): | |
| _sanity_check_server_args(actual_server_args, expect_server_args) | ||
|
|
||
| def _init_normal(self, server_args_dict): | ||
| logger.info(f"Launch HttpServerEngineAdapter at: {self.server_host}:{self.server_port}") | ||
| self.process = launch_server_process(ServerArgs(**server_args_dict)) | ||
| use_rdt = self.args.update_weight_transfer_mode == "rdt" | ||
| if use_rdt: | ||
| if self.node_rank != 0: | ||
| # For a multi-node engine, the node-0 server's RayEngine spawns | ||
| # the SchedulerActors of ALL ranks (placed cross-node via the | ||
| # placement group), so non-zero node ranks launch nothing. | ||
| self.process = None | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This breaks Miles' contract of using the process to control the lifecycle of server engines, which will conflict with the process-based control logics, including cleanup, fault tolerance, etc...
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In non-rdt path, we also return None for the process for non-zero node ranks The engine only needs the process on rank 0 node |
||
| return | ||
| server_args_dict["use_ray"] = True | ||
| server_args_dict["enable_rdt_weight_sync"] = True | ||
| assert self.placement_group is not None and self.pg_bundles | ||
| logger.info( | ||
| f"Launch HttpServerEngineAdapter at: {self.server_host}:{self.server_port}" | ||
| f"{' (use_ray=True for RDT)' if use_rdt else ''}" | ||
| ) | ||
| server_args = ServerArgs(**server_args_dict) | ||
| if use_rdt: | ||
| envs.SGLANG_RAY_BUNDLE_INDICES.set(",".join(str(bundle) for bundle in self.pg_bundles)) | ||
| server_args.override( | ||
| "miles.rdt.ray_context", | ||
| placement_group=self.placement_group, | ||
| ray_runtime_env=dict(ray.get_runtime_context().runtime_env), | ||
| ) | ||
| self.process = launch_server_process(server_args) | ||
|
|
||
| if self.node_rank == 0 and self.router_ip and self.router_port: | ||
| if parse(sglang_router.__version__) <= parse("0.2.1") or self.args.use_miles_router: | ||
|
|
@@ -465,6 +500,9 @@ def flush_cache(self): | |
| def shutdown(self): | ||
| if self.args.rollout_external: | ||
| return | ||
| if getattr(self, "process", None) is None: | ||
| # Non-zero node ranks of an RDT multi-node engine launch no server. | ||
| return | ||
|
|
||
| logger.info(f"Shutdown engine {self.server_host}:{self.server_port}...") | ||
| if self.node_rank == 0: | ||
|
|
@@ -514,6 +552,51 @@ def unload_lora_adapter(self, lora_name: str): | |
| {"lora_name": lora_name}, | ||
| ) | ||
|
|
||
| def get_scheduler_actors(self) -> list: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function looks a bit of hardcoded
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. addressed hardcoding by adding |
||
| """Return this engine's SchedulerActor handles (RDT mode, use_ray=True). | ||
|
|
||
| RayEngine names one actor per (pp, tp) rank | ||
| ``sglang_scheduler_node{ip}[_dp{dp}]_pp{pp}_tp{tp}_port{port}_pg{hex}_bundle{idx}``. | ||
| The pg/bundle suffix is unknown here, so match on node IP, HTTP port, | ||
| and TP rank across namespaces. Ports are allocated per host, making | ||
| ``(node IP, port)`` the engine's unique key. Raises unless every TP rank | ||
| matches exactly one actor: a partial list would silently sync a subset. | ||
| """ | ||
| if self._scheduler_actors: | ||
| return self._scheduler_actors | ||
|
|
||
| tp_size = getattr(self.args, "rollout_num_gpus_per_engine", 1) | ||
| node_prefix = f"sglang_scheduler_node{ray.util.get_node_ip_address()}_" | ||
| port_token = f"_port{self.server_port}_" | ||
|
|
||
| try: | ||
| raw = ray.util.list_named_actors(all_namespaces=True) | ||
| except TypeError: | ||
| # Older Ray without the all_namespaces kwarg. | ||
| raw = ray.util.list_named_actors() | ||
| entries = [(e["name"], e.get("namespace")) if isinstance(e, dict) else (e, None) for e in raw] | ||
| # Kept for the failure message below. | ||
| sched_like = [(n, ns) for (n, ns) in entries if "scheduler" in n.lower() or "sglang" in n.lower()] | ||
| engine_entries = [(n, ns) for (n, ns) in entries if n.startswith(node_prefix) and port_token in n] | ||
|
|
||
| actors = [] | ||
| for tp_rank in range(tp_size): | ||
| tp_token = f"_pp0_tp{tp_rank}_" | ||
| matches = [(n, ns) for (n, ns) in engine_entries if tp_token in n] | ||
| if len(matches) != 1: | ||
| raise RuntimeError( | ||
| f"SchedulerActor discovery for engine {self.server_host}:{self.server_port} " | ||
| f"tp_rank={tp_rank} matched {len(matches)} actors (expected 1): " | ||
| f"{[n for n, _ in matches]}. tokens: '{node_prefix}', '{port_token}', " | ||
| f"'{tp_token}'. Discovered {len(entries)} named actors, " | ||
| f"{len(sched_like)} scheduler-like: {[n for n, _ in sched_like[:20]]}." | ||
| ) | ||
| name, namespace = matches[0] | ||
| actors.append(ray.get_actor(name, namespace=namespace) if namespace else ray.get_actor(name)) | ||
|
|
||
| self._scheduler_actors = actors | ||
| return actors | ||
|
|
||
| def release_memory_occupation(self, tags: list[str] = None): | ||
| """Release memory occupation. Available tags: weights, kv_cache.""" | ||
| self.flush_cache() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
importing ray here and using placement group does not match the files' scope, these are usually controlled under /ray
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGLangEngine subclasses miles.ray.ray_actor.RayActor
placement_group removed, ray only imported at method level