Prhub

#1313 RDT weight sync: GPU->GPU zero copy transfer through SGLang Ray actor backend

原始 PR 作者 xyuzh 合并时间 2026-08-23 08:00 文件变更 15 提交数 21 评论 48 代码增减 +793 / -21

执行摘要

新增 RDT/NIXL GPU 零拷贝权重同步传输

PR body 的动机表述是:RDT 复用 P2P 与 NCCL 路径的 bucketed all-gather + HF 转换,只把 bucket 载荷改走 NIXL(ray.put + RDMA pull),从而 'instead of holding a full GPU replica of the rollout model';作者强调 'We have measured faster weight sync speed across models than P2P RDMA and NCCL',并用 --check-weight-update-equal 验证逐字节相等。关联 Issue 讨论中 yueming-yuan 提出多节点 SGLang engine 与 e2e CI 测试两项诉求,stephanie-wang 追问 'Also do we have tests?',两者共同构成了本 PR 的验收范围。

值得精读,尤其是 miles/backends/megatron_utils/update_weight/update_weight_from_rdt.py 与 miles/ray/rollout/sglang_server_actor.py:前者给出了“固定 bucket + 视图重定向 + 终身 NIXL 注册 + 分轮 flush”这一套内存受限零拷贝发布范式,后者演示了如何用同 job 子 actor 规避跨 job named actor 发现。建议关注三个设计决策:一是 expandable_segments 与 CUDA-IPC/NIXL 的性能悬崖(约 0.3 GB/s vs 约 150 GB/s,是全 PR 最有价值的工程洞察);二是 bucket 终身注册反过来约束了应对超大参数的策略(先扩容、再分轮);三是 max_concurrency 提升与 FT concurrency group 的相互作用。对不打算启用 rdt 的读者,重点是确认 rdt 特化的 PG 排序、num_gpus_per_actor 分支与环境变量注入不会污染默认路径;同时应跟踪三个未闭合项:ray/mp 后端校验、异质 engine count 的 per-engine 计划、以及 actor 化后的一致性清理与容错。

讨论亮点

讨论集中在四类问题。其一是新增依赖的边界:stephanie-wang 追问 'Where is pull_weights defined?',xyuzh 指向 sglang 侧 PR #27723;她又问 'Why do we have both this flag and the --update-weight-transfer-mode option?',作者直接回复 'removed',最终只保留枚举作为唯一入口;同样由她提出的 'Also need to check that we're using ray as the backend and not mp?' 只得到 'yeah, that shall be enforced in the running scripted',未落到参数校验。其二是设计冲突:maocheng23 在 placement_group.py 提 'Would this be conflict with PPO?',guapisolo 确认 'this deterministically conflicts with Shared Actor/Critic PPO',并推送 e2f87822a1 在参数校验中拒绝该组合。其三是生命周期契约:yueming-yuan 指出同 job actor 托管 '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',作者以非 RDT 路径在非 0 节点同样不持有 process 回应。其四是健壮性与测试:gemini-code-assist 与 guapisolo 分别指出 bucket 溢出崩溃(后者给出 embed_tokens 622,329,856 bytes 的具体算例)、异质 engine GPU counts 会规划出越界 rank、fast 测试 fixture 缺字段导致 AttributeError;guapisolo 还先提了 concurrency group 的 P1 又自行撤回('I traced this through Ray 2.56 and retract this P1')。作者逐条修复,并在 bucket 扩容上主动增强 CI 覆盖:'Removed the hand-tuned 1 GiB override from the 30B e2e test... forces the new growth path to trigger on the 0.58 GiB embed_tokens in every CI run'。

实现拆解

  1. 新增传输实现 UpdateWeightFromRDT(miles/backends/megatron_utils/update_weight/update_weight_from_rdt.py,新增 430 行):继承 DistBucketedWeightUpdateMixin,复用与 P2P 相同的 bucketed TP/EP all-gather 与 HF 转换,仅替换最后一段字节搬运。connect_rollout_engines() 先拒绝非均匀 engine GPU counts(raise ValueError),_is_source 只让 _gathered_dp_rank < _rollout_num_gpus 的 rank 参与发送;随后按 engine_rank 分组,通过 get_parallelism_info() / get_server_info() 构造 RankParallelismConfig 与 ServerArgs,为每个 engine rank 建一个 _EngineRankBucket。create_gpu_replica() 用 load_format="dummy" 短暂分配整模型,记录 (shape, dtype, nbytes) 后释放 storage,并同时 patch 模块级与实例级 post_load_weights,把权重后处理留给 rollout 侧的 end_weight_update()。影响面:miles/backends/megatron_utils/actor.py 在 rdt 模式下优先选择 UpdateWeightFromRDT,p2p_transfer_utils.RemoteTransferPlan 被复用。
  2. bucket 打包与分轮发布(同文件 _EngineRankBucket / _staging_span):stage() 用 _staging_span 做 dtype 对齐,把 params_dict[name].data 重定向到 bucket 内部连续视图,于是 load_weights() 的写入结果本身就在 bucket 里,ray.put(views, _tensor_transport="nixl") 即零拷贝发布。bucket 分配时临时关闭 expandable_segments(VMM 显存无法导出 CUDA-IPC handle,会让 UCX 静默丢掉 cuda_ipc lane)、按 max(--update-weight-buffer-size, 最大目标参数) 扩容并告警,再用 ray.experimental.register_nixl_memory 终身注册以避免每次 flush 重新握手。因为注册终身有效、bucket 不能再增长,最终 commit dbc55f24 把超 bucket 的 flush 拆成多轮顺序 NIXL 传输,_get_transfer_ready_params 改为返回 name->shards 让每轮只携带自己的 HF 张量。
  3. 引擎托管与 SchedulerActor 句柄(新增 miles/ray/rollout/sglang_server_actor.py,修改 miles/backends/sglang_utils/sglang_engine.py):SGLangServerActor.start() 在同 job 子 actor 内调用 sglang 的 launch_engine(server_args, placement_group=...),在子进程里设置 SGLANG_RAY_BUNDLE_INDICES,把 uvicorn 放到 daemon 线程,从而把 scheduler_actors 句柄直接回传给 miles;is_alive() 供健康探测使用。_launch_sglang_server() 以 num_cpus=0.2、num_gpus=0 和 PlacementGroupSchedulingStrategy(..., placement_group_capture_child_tasks=True) 启动该 actor;SGLangEngine._init_normal() 在 rdt 下强制 use_ray=True、enable_rdt_weight_sync=True,多节点引擎只在 node_rank 0 启动,shutdown() 走 ray.kill(self._sglang_server_actor)。get_scheduler_actors() 供 _get_engine_scheduler_actors() 调用,并对每个 actor 执行 register_weight_for_rdt() 保活目标侧注册。
  4. 资源布局与并发(miles/ray/placement_group.py、miles/ray/rollout/server_group.py、miles/ray/train/actor_factory.py):_create_placement_group(num_gpus, is_rdt) 在 rdt 下按“节点已填充 bundle 数降序”重排,使 trainer 占满整节点、rollout bundle 落在仍有空闲 GPU 的节点,避免 sglang RayEngine STRICT_PACK 死锁;allocate_train_group() 在 rdt 下把 num_gpus_per_actor 从 0.4 提到 1(每个 trainer rank 独占物理 GPU);ServerGroup.start_engines() 给引擎 node-0 rank 传 pg_bundles=[...],并把引擎协调 actor 的 num_gpus 从 0.2 降到 0;allocate_gpus_for_actor() 设置 RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES=0、PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True、MILES_RDT_NIXL_VALIDATE_BYTES,并把 max_concurrency 提到 1 + rollout_num_gpus_per_engine 以免 update_weights() 阻塞饿死 NIXL 传输线程。
  5. 参数、依赖与测试配套:miles/utils/arguments.py 的 --update-weight-transfer-mode 增加 rdt 选项,miles_validate_args() 断言 rdt 仅支持 megatron、拒绝 use_critic(Shared Actor/Critic PPO),并与 p2p 一起纳入禁 colocate / 禁 PD / 禁 LoRA 检查;测试侧新增 tests/e2e/megatron/test_qwen3_30B_A3B_rdt.py(8 卡、TP1·CP2·PP3、EP2、6 actor + 2 rollout 非 colocate、5 个 register_ci_gate 指标)、tests/fast/utils/test_arguments.py::TestRdtValidation,并给 tests/fast/ray/rollout/conftest.py::make_args() 与 tests/fast/ray/test_actor_factory.py 补 update_weight_transfer_mode 字段;requirements.txt 上调 ray 版本以匹配 set_target_for_ref 依赖。
文件 模块 状态 重要度
miles/backends/megatron_utils/update_weight/update_weight_from_rdt.py 权重同步 added 9.18
miles/backends/sglang_utils/sglang_engine.py 推理引擎 modified 8.02
miles/ray/rollout/sglang_server_actor.py 引擎托管 added 8.22
miles/ray/placement_group.py 资源编排 modified 6.85
miles/ray/rollout/server_group.py 引擎编排 modified 6.28
miles/ray/train/actor_factory.py 训练角色 modified 6.39
miles/utils/arguments.py 参数校验 modified 6.37
miles/backends/megatron_utils/actor.py 训练器 modified 5.84
tests/e2e/megatron/test_qwen3_30B_A3B_rdt.py 端到端测试 added 6.98
tests/fast/utils/test_arguments.py 参数测试 modified 6.34
requirements.txt 依赖声明 modified 1.72

关键符号

UpdateWeightFromRDT.connect_rollout_engines UpdateWeightFromRDT.create_gpu_replica UpdateWeightFromRDT._get_engine_scheduler_actors UpdateWeightFromRDT._is_source UpdateWeightFromRDT.mark_engine_connection_stale _EngineRankBucket.stage _staging_span SGLangServerActor.start SGLangServerActor.is_alive _launch_sglang_server SGLangEngine._init_normal SGLangEngine.get_scheduler_actors SGLangEngine.shutdown _create_placement_group allocate_train_group allocate_gpus_for_actor ServerGroup.start_engines miles_validate_args

关键源码片段

miles/backends/megatron_utils/update_weight/update_weight_from_rdt.py core-logic

本 PR 的核心实现:`UpdateWeightFromRDT` 继承 `DistBucketedWeightUpdateMixin`,用固定 GPU bucket + 视图重定向 + `ray.put(_tensor_transport="nixl")` 实现零拷贝 RDMA 发布,并包含 bucket 扩容、VMM 规避、NIXL 终身注册等关键工程决策。

固定 bucket 的打包协议

stage() 把参数存储整体迁进 bucket 视图,这是后续零拷贝发布成立的前提。

def _staging_span(offset: int, dtype: torch.dtype, nbytes: int) -> tuple[int, int]:
    """(aligned start, end) for placing nbytes of dtype at offset in the uint8 bucket."""
    # 唯一一份对齐算法:打包方与 stage() 共用,避免两边算出不同的 offset
    start = (offset + dtype.itemsize - 1) // dtype.itemsize * dtype.itemsize
    return start, start + nbytes
​
​
class _EngineRankBucket:
    """一个 engine rank 的传输上下文:模型 replica、固定 bucket、参数规格与拉取方 actor。"""
​
    def stage(self, names: list[str]) -> list[torch.Tensor]:
        """把 params_dict[name].data 重定向到 bucket 内的连续视图,按 names 顺序返回视图。"""
        offset = 0
        views: list[torch.Tensor] = []
        capacity = self.gpu_bucket.numel() # bucket 是 uint8,numel 即字节容量
        for name in names:
            shape, dtype, nbytes = self.param_specs[name]
            offset, end = _staging_span(offset, dtype, nbytes)
            assert end <= capacity, (
                f"[RDT] Bucket overflow while staging '{name}': "
                f"need {end} bytes but bucket is {capacity} bytes. "
                f"Increase --update-weight-buffer-size."
            )
            # 关键一步:之后 load_weights() 写进 params_dict[name].data 的字节
            # 实际上就落在 bucket 里,发布这个视图等价于发布权重本身
            view = self.gpu_bucket[offset:end].view(dtype).reshape(shape)
            self.params_dict[name].data = view
            views.append(view)
            offset = end
        return views

bucket 分配:绕开 VMM 的 CUDA-IPC 限制并终身注册

这段代码同时承载了性能悬崖修复、超大参数扩容和 NIXL 注册保活三个约束。

            # CRITICAL:expandable(VMM / cuMemCreate)显存无法导出 legacy CUDA-IPC handle,
            # UCX 会静默丢掉 cuda_ipc lane,NIXL 退化成 TCP 上的软件 RMA —— 约 0.3 GB/s,
            # 而 NVLink 上可达约 150 GB/s。作者最终二分定位到 PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True。
            expandable = "expandable_segments:True" in os.environ.get("PYTORCH_CUDA_ALLOC_CONF", "")
            if expandable:
                torch._C._accelerator_setAllocatorSettings("expandable_segments:False")
            try:
                # 单个目标参数可能比 --update-weight-buffer-size(默认 512 MiB)更大:
                # TP=1 的 Qwen3-30B-A3B embed_tokens 约 622,329,856 bytes(~0.58 GiB),
                # 这里按 max( 配置值 , 最大参数 ) 扩容并告警,而不是让首次 flush 直接断言崩溃
                max_param_nbytes = max((spec[2] for spec in param_specs.values()), default=0)
                if max_param_nbytes > self.args.update_weight_buffer_size:
                    logger.warning(
                        f"[RDT] Largest destination parameter needs {max_param_nbytes} bytes, above "
                        f"--update-weight-buffer-size ({self.args.update_weight_buffer_size}); "
                        f"growing bucket to fit."
                    )
                gpu_bucket = torch.empty(
                    max(self.args.update_weight_buffer_size, max_param_nbytes),
                    dtype=torch.uint8,
                    device=torch.cuda.current_device(),
                )
            finally:
                if expandable:
                    torch._C._accelerator_setAllocatorSettings("expandable_segments:True")
​
            # 终身 pin 住 NIXL 注册:否则每次 ray.put 都会重新注册 bucket,ref 释放会抬高
            # NIXL agent meta version,迫使所有 engine 每个 flush 重新握手。
            # 代价是 bucket 不能再增长 —— 这直接催生了后面的“分轮 flush”设计。
            from ray.experimental import register_nixl_memory
            register_nixl_memory(gpu_bucket)
miles/backends/sglang_utils/sglang_engine.py core-logic

RDT 模式的引擎启动入口:新增 `_launch_sglang_server()` 在同 job 子 actor 中托管 HTTP server 并直接取回 SchedulerActor 句柄,`_init_normal()` 在 rdt 下强制 use_ray 且多节点只在 node 0 启动,`shutdown()` 改为 ray.kill。

rdt 模式的引擎启动与 SchedulerActor 句柄获取

下面把模块级 _launch_sglang_server() 与 SGLangEngine 中的相关分支放在一起,能看清句柄是怎么一路传到权重同步层的。

def _launch_sglang_server(server_args: ServerArgs, bundle_indices: list[int]):
    """Host the Ray HTTP server in a same-job child actor. Returns (actor, scheduler_actors)."""
    server_args.host = server_args.host.strip("[]")
    # 同一个 Ray job 内起子 actor:SchedulerActor 句柄由 start() 直接返回,
    # 不再需要跨 job 扫描 named actor,也就没有 namespace / port 匹配歧义
    placement_group = ray.util.get_current_placement_group()
    assert placement_group is not None
    http_actor = (
        ray.remote(SGLangServerActor)
        .options(
            num_cpus=0.2,
            num_gpus=0, # 只托管 HTTP 服务,不占 GPU
            scheduling_strategy=PlacementGroupSchedulingStrategy(
                placement_group=placement_group,
                placement_group_capture_child_tasks=True, # SchedulerActor 作为子任务复用 miles 的 PG
                placement_group_bundle_index=bundle_indices[0],
            ),
        )
        .remote()
    )
    scheduler_actors = ray.get(http_actor.start.remote(server_args, bundle_indices=bundle_indices))
    _wait_server_healthy(
        base_url=server_args.url(),
        api_key=server_args.api_key,
        is_process_alive=lambda: ray.get(http_actor.is_alive.remote()),
    )
    return http_actor, scheduler_actors
​
​
class SGLangEngine(RayActor):
    def _init_normal(self, server_args_dict):
        use_rdt = self.args.update_weight_transfer_mode == "rdt"
        if use_rdt:
            if self.node_rank != 0:
                # 多节点引擎:node-0 的 RayEngine 会跨节点拉起所有 rank 的 SchedulerActor,
                # 因此非 0 节点这里什么都不启动(非 RDT 路径的 process 同样只在 node 0 有效)
                return
            server_args_dict["use_ray"] = True
            server_args_dict["enable_rdt_weight_sync"] = True
            assert self.pg_bundles # 必须由 ServerGroup 传入 miles 已预留的 bundle 列表
        server_args = ServerArgs(**server_args_dict)
        if use_rdt:
            self._sglang_server_actor, self._scheduler_actors = _launch_sglang_server(
                server_args, bundle_indices=self.pg_bundles
            )
        else:
            self.process = launch_server_process(server_args)
​
    def get_scheduler_actors(self) -> list:
        """Return this engine's SchedulerActor handles (RDT mode, use_ray=True)."""
        # UpdateWeightFromRDT 通过它拿到 pull_weights 的目标 actor,并对每个 actor 做 register_weight_for_rdt
        return self._scheduler_actors
miles/ray/rollout/sglang_server_actor.py core-logic

新增同 job actor 来托管 sglang HTTP server,是最终绕开跨 job/namespace named actor 发现、并让 SchedulerActor 句柄可直接回传的关键;同时界定了 uvicorn 线程生命周期与子任务清理关系。

同 job 托管:句柄直传而不是按名字查找

class SGLangServerActor:
    """Miles-owned process that hosts the SGLang HTTP server (RDT / use_ray)."""
​
    def start(self, server_args: ServerArgs, bundle_indices: list[int]) -> list:
        from sglang.srt.ray.http_server import launch_engine, serve_http
​
        # 必须在本进程内设置:父 actor 对 os.environ 的修改不会传进子进程
        # (这正是 7bbb20cc 修掉的真实 bug)
        envs.SGLANG_RAY_BUNDLE_INDICES.set(",".join(str(i) for i in bundle_indices))
        placement_group = ray.util.get_current_placement_group()
        assert placement_group is not None
        engine = launch_engine(server_args, placement_group=placement_group)
        _, _, _, scheduler_init_result, _ = engine
        # uvicorn 是阻塞调用,放到 daemon 线程里跑,start() 才能先把句柄返回给 miles
        self._serve_thread = threading.Thread(
            target=serve_http,
            args=(engine, server_args),
            daemon=True,
            name="sglang-uvicorn",
        )
        self._serve_thread.start()
        return list(getattr(scheduler_init_result, "scheduler_actors", None) or [])
​
    def is_alive(self) -> bool:
        # _wait_server_healthy 用它做存活探测;kill 掉这个 actor 会连带拆掉子任务中的 SchedulerActor
        return self._serve_thread is not None and self._serve_thread.is_alive()

评论区精华

RDT 与 Shared Actor/Critic PPO 是否冲突 设计

maocheng23 在 miles/ray/placement_group.py:149 提问 'Would this be conflict with PPO? @guapisolo to double confirm'。guapisolo 复查后确认:RDT 每个 trainer rank 独占整张 GPU,而 PPO 复用 actor 的 placement group bundle 安置 critic rank,actor 初始化后 critic actor 无法调度。

结论:e2f87822a1 在参数校验中直接拒绝 rdt + PPO 并补 parser 级测试;支持该组合需要独立 critic GPU bundle,属后续工作。 · 已解决

固定 bucket 溢出:动态扩容与分轮 flush 正确性

gemini-code-assist 先指出单参数超过 --update-weight-buffer-size 会在首次 flush 断言崩溃;guapisolo 给出具体算例 '[P2] ... a legal TP=1 Qwen3-30B-A3B rollout has a bf16 embed_tokens parameter of 622,329,856 bytes (~0.58 GiB)',并建议在 connect_rollout_engines 阶段就扩容或拒绝。

结论:bucket 按 max( 配置值 , 最大目标参数 nbytes) 扩容并打 warning,e2e 测试去掉手调的 1 GiB 覆盖;随后 dbc55f24 把超出 bucket 的 flush 拆成多轮顺序 NIXL 传输,容量不再限制单次同步。 · 已解决

SchedulerActor 发现方式:named actor 扫描 → 名称派生 → 同 job 句柄直传 设计

yueming-yuan 评价 `get_scheduler_actors` 'looks a bit of hardcoded';gemini-code-assist 补充 127.0.0.1 / 0.0.0.0 场景下按 host 匹配会失败;xyuzh 先改用 sglang 的 `get_scheduler_actor_name` 重算名称(0150527b),随后把 HTTP server 搬进同 job 的 `SGLangServerActor`(e0f0979a)。期间 4c5a3513 删除 PG 复用管线、f8bc7050 又回滚,最终 f9619174 通过 ServerGroup 传 pg_bundles 打通。

结论:最终以同 job actor 的句柄直传为主,跨 job / namespace 发现被移除;但 PR body 里仍写着按 _port{port}_ / _tp{rank}_ token 匹配的旧描述,需要同步修正。 · resolved-with-followup

RDT 是否破坏 miles 的进程式引擎生命周期契约 设计

yueming-yuan:'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...'。xyuzh 回应非 RDT 路径在非 0 节点同样返回 None process,节点 0 才真正需要进程。

结论:通过新增 SGLangServerActor 与 shutdown() 中的 ray.kill 分支缓解,但进程与 actor 两套生命周期并存的清理 / 容错行为没有在 e2e 之外验证,FT 场景仍属未证实。 · partially-resolved

异质 engine GPU counts 的处理 正确性

guapisolo [P2]:--sglang-config 允许每个 server group 覆盖 num_gpus_per_engine,但 connect_rollout_engines 忽略传入的 engine_gpu_counts,仍按全局 args.rollout_num_gpus_per_engine 构建 RemoteTransferPlan,会生成越界 scheduler rank 或漏掉某个 engine。

结论:xyuzh 加入 ValueError 快速失败(拒绝非均匀 counts),并把 per-engine 传输计划明确推迟到出现真实用例时;属于有意保留的限制。 · partially-resolved-deferred

是否校验 rollout 使用 ray 而非 mp 后端 question

stephanie-wang 在 miles_validate_args 处提问 'Also need to check that we're using ray as the backend and not mp?'。

结论:xyuzh 回复 'yeah, that shall be enforced in the running scripted',即落在运行脚本约定,参数校验层至今没有对应断言。 · unresolved

fast 测试 fixtures 缺 update_weight_transfer_mode 导致既有测试失败 测试

guapisolo [P2]:allocate_gpus_for_actor 与 ServerGroup.start_engines 无条件读取该字段,tests/fast/ray/test_actor_factory.py 的 SimpleNamespace 与 tests/fast/ray/rollout/conftest.py::make_args() 会抛 AttributeError。

结论:两处 fixture 补 update_weight_transfer_mode="broadcast",与 parser 默认值对齐。 · 已解决

max_concurrency 提升与 FT concurrency group 的关系 性能

guapisolo 先提 P1,随后自行撤回:'I traced this through Ray 2.56 and retract this P1 ... the non-FT path needs max_concurrency > 1 only to force that same off-main-thread executor',并指出 NIXL 是单边传输、SchedulerActor 侧另有并发设置。

结论:保留 elif args.update_weight_transfer_mode == "rdt" 分支,作者回复 'kept the concurrency setup as-is'。 · resolved-retracted

--use-rdt-weight-sync 别名与 --update-weight-transfer-mode 重复 设计

stephanie-wang:'Why do we have both this flag and the --update-weight-transfer-mode option?',认为两个入口容易造成单一真相源分裂。

结论:作者回复 'removed',最终只保留 --update-weight-transfer-mode 的 rdt 枚举值作为唯一开关。 · 已解决

风险与影响

  1. 兼容面被显著收窄:miles/utils/arguments.py 强制 rdt ⊕ colocate / PPO / FSDP / PD / LoRA 互斥,用户必须重新规划 GPU 池(e2e 即用 6 actor + 2 rollout 的非 colocate 布局),误配只能靠断言拦下。
  2. 参数校验留白:review 明确提出的“校验 rollout 后端是 Ray 而非 mp”没有落到 miles_validate_args,只在运行脚本层面约定,配置错误可能延后到运行时才暴露。
  3. 异质 engine 布局被硬拒绝:connect_rollout_engines() 对 engine_gpu_counts 不一致直接 ValueError,--sglang-config 覆盖 num_gpus_per_engine 的用法在 rdt 下不可用,per-engine transfer plan 属明确推迟项。
  4. 私有/实验 API 与外部分支耦合:torch._C._accelerator_setAllocatorSettings、ray.experimental.register_nixl_memory、set_target_for_ref、sglang 的 launch_engine / enable_rdt_weight_sync 均为非稳定接口,且依赖 sglang-miles 分支与 ray>=2.55.1(requirements 已上调),升级或回退会双向耦合。
  5. 全局状态与 monkey patch:create_gpu_replica() 期间替换 sglang.srt.model_loader.loader.post_load_weights 模块级函数并调用 set_global_server_args_for_scheduler(),虽有 try/finally 恢复,但对并发/异常路径不具线程安全性;NIXL 注册终身有效也意味着 GPU 内存不可回收。
  6. 生命周期与容错:node_rank != 0 不再持有 process,shutdown() 依赖 ray.kill(self._sglang_server_actor) 连带清理子任务中的 SchedulerActor;yueming-yuan 的 FT/清理顾虑只被部分回应,e2e 之外没有覆盖故障场景。
  7. 测试覆盖不均:bucket 打包对齐(_staging_span)、分轮 flush 与 _EngineRankBucket.stage 只有 est_time=700 的 8 卡 e2e 与参数级 fast 测试保护,缺少可直接跑在 fast-gpu 上的一致性单测。
  8. 文档与实现不一致:PR body 仍描述按 _port{port}_ / _tp{rank}_ token 匹配 named actor 与 MILES_RDT_REUSE_PG 复用,而 head 版本已改为同 job SGLangServerActor 直接回传句柄(提交历史也有一次删除后再激活 PG 复用的反复),后续需要同步说明。

对用户:新增一个 opt-in 的 --update-weight-transfer-mode rdt,带来权重同步的实质加速(GLM-Z1-9B 0.40s vs NCCL 0.69s、GLM-4.5-Air 2.30s vs 5.00s)并消除 CPU 全模型副本带来的主机内存压力;代价是必须使用 sglang-miles 分支与 ray>=2.55.1、非 colocate 布局、禁用 PPO/FSDP/PD/LoRA,且跨节点需 EFA + LIBFABRIC 用户态栈。对系统:在权重同步层形成 broadcast / p2p / disk-delta / rdt 四传输并存格局,并首次把 rollout 引擎的 HTTP server 托管为 miles Ray actor,改变了引擎的进程/actor 生命周期与 placement group 排序规则(is_rdt 分支会影响同一 PG 内所有角色的落点)。对团队:引入了对 sglang 集成分支(#27723、#34621)的持续跟进成本,后续 sglang 接口变动会直接冲击 _launch_sglang_server、create_gpu_replica 与 pull_weights 调用点;同时把 RDT 的验证负担压在了单条长耗时 e2e 上。

核心权重同步路径变更 依赖 sglang-miles 集成分支 私有与实验 API 依赖 参数校验留白(ray/mp 后端未断言) 异质 engine 布局被硬拒 fast 单测缺失(打包与分轮逻辑) 进程式生命周期契约变更

关联 Issue

#27723 [sglang-miles] RDT/NIXL weight sync support for Ray scheduler actors
#34621 [ray] expose get_scheduler_actor_name so callers can look up the schedulers

完整报告

参与讨论