执行摘要
- 一句话:新增 RDT/NIXL GPU 零拷贝权重同步传输
- 推荐动作:值得精读,尤其是
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 化后的一致性清理与容错。
功能与动机
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 的验收范围。
实现拆解
- 新增传输实现
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 被复用。
- 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 张量。
- 引擎托管与 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() 保活目标侧注册。
- 资源布局与并发(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 传输线程。
- 参数、依赖与测试配套:
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(模块 权重同步;类别 source;类型 core-logic;符号 _staging_span, _EngineRankBucket, _EngineRankBucket.init, _EngineRankBucket.stage): 本 PR 的核心实现:UpdateWeightFromRDT 继承 DistBucketedWeightUpdateMixin,用固定 GPU bucket + 视图重定向 + ray.put(_tensor_transport="nixl") 实现零拷贝 RDMA 发布,并包含 bucket 扩容、VMM 规避、NIXL 终身注册等关键工程决策。
miles/backends/sglang_utils/sglang_engine.py(模块 推理引擎;类别 source;类型 core-logic;符号 _launch_sglang_server, SGLangEngine.init, SGLangEngine._init_normal, SGLangEngine.shutdown): RDT 模式的引擎启动入口:新增 _launch_sglang_server() 在同 job 子 actor 中托管 HTTP server 并直接取回 SchedulerActor 句柄,_init_normal() 在 rdt 下强制 use_ray 且多节点只在 node 0 启动,shutdown() 改为 ray.kill。
miles/ray/rollout/sglang_server_actor.py(模块 引擎托管;类别 source;类型 core-logic;符号 SGLangServerActor, SGLangServerActor.init, SGLangServerActor.start, SGLangServerActor.is_alive): 新增同 job actor 来托管 sglang HTTP server,是最终绕开跨 job/namespace named actor 发现、并让 SchedulerActor 句柄可直接回传的关键;同时界定了 uvicorn 线程生命周期与子任务清理关系。
miles/ray/placement_group.py(模块 资源编排;类别 source;类型 core-logic;符号 _create_placement_group, create_placement_groups, allocate_train_group): rdt 模式下改变 bundle 排序与 actor GPU 预留:_create_placement_group(is_rdt=True) 让 trainer 优先占满整节点以保证 rollout 落点,allocate_train_group() 把 num_gpus_per_actor 从 0.4 提到 1,是 PPO 冲突讨论的源头。
miles/ray/rollout/server_group.py(模块 引擎编排;类别 source;类型 core-logic;符号 ServerGroup.start_engines): 把引擎已预留的整份 bundle 列表以 pg_bundles 传给 node-0 引擎并降低协调 actor 的 GPU 占用,是 RDT 不再二次创建 placement group、避免 rollout GPU 被双订的接线点。
miles/ray/train/actor_factory.py(模块 训练角色;类别 source;类型 configuration;符号 allocate_gpus_for_actor): 集中注入 RDT 运行环境与并发参数:保留 Ray 的 CUDA_VISIBLE_DEVICES 掩码、强制 expandable_segments、按 bucket 大小探测 NIXL 后端,并把 max_concurrency 提到 1+tp_size 以防传输线程被阻塞调用饿死。
miles/utils/arguments.py(模块 参数校验;类别 source;类型 configuration;符号 add_rollout_arguments, miles_validate_args): 新增 rdt 传输模式选项并定义支持边界:megatron-only、禁止 use_critic、沿用 p2p 的禁 colocate / 禁 PD / 禁 LoRA 断言,是用户接触该特性的第一道闸门。
miles/backends/megatron_utils/actor.py(模块 训练器;类别 source;类型 dependency-wiring;符号 init): 权重同步实现的装配点:在 init() 中依据 update_weight_transfer_mode == "rdt" 选择 UpdateWeightFromRDT,并置于 colocate 分支之前,决定了整个训练侧的同步行为。
tests/e2e/megatron/test_qwen3_30B_A3B_rdt.py(模块 端到端测试;类别 test;类型 test-coverage;符号 prepare, execute): RDT 唯一的端到端验证:8 卡上以非 colocate 的 6 actor + 2 rollout 布局、TP1·CP2·PP3 与 5 个 CI 指标门验证权重同步,并按 reviewer 要求对齐 p2p 测试格式。
tests/fast/utils/test_arguments.py(模块 参数测试;类别 test;类型 test-coverage;符号 TestRdtValidation, TestRdtValidation._validate, TestRdtValidation.test_accepts_megatron_without_critic, TestRdtValidation.test_rejects_unsupported_configuration): 为 rdt 的支持边界提供快速回归保护:覆盖 megatron 无 critic 的合法路径,以及 FSDP 与 PPO 两种拒绝路径。
requirements.txt(模块 依赖声明;类别 config;类型 configuration): 上调 ray 版本以匹配 RDT 依赖的 ray.experimental 接口,是运行环境能否启动该特性的前提。
关键符号: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
本 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
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
新增同 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()
评论区精华
讨论集中在四类问题。其一是新增依赖的边界: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'。
- RDT 与 Shared Actor/Critic PPO 是否冲突 (design): e2f87822a1 在参数校验中直接拒绝 rdt + PPO 并补 parser 级测试;支持该组合需要独立 critic GPU bundle,属后续工作。
- 固定 bucket 溢出:动态扩容与分轮 flush (correctness): bucket 按 max(配置值, 最大目标参数 nbytes) 扩容并打 warning,e2e 测试去掉手调的 1 GiB 覆盖;随后 dbc55f24 把超出 bucket 的 flush 拆成多轮顺序 NIXL 传输,容量不再限制单次同步。
- SchedulerActor 发现方式:named actor 扫描 → 名称派生 → 同 job 句柄直传 (design): 最终以同 job actor 的句柄直传为主,跨 job / namespace 发现被移除;但 PR body 里仍写着按 port{port} / tp{rank} token 匹配的旧描述,需要同步修正。
- RDT 是否破坏 miles 的进程式引擎生命周期契约 (design): 通过新增 SGLangServerActor 与 shutdown() 中的 ray.kill 分支缓解,但进程与 actor 两套生命周期并存的清理/容错行为没有在 e2e 之外验证,FT 场景仍属未证实。
- 异质 engine GPU counts 的处理 (correctness): xyuzh 加入 ValueError 快速失败(拒绝非均匀 counts),并把 per-engine 传输计划明确推迟到出现真实用例时;属于有意保留的限制。
- 是否校验 rollout 使用 ray 而非 mp 后端 (question): xyuzh 回复 'yeah, that shall be enforced in the running scripted',即落在运行脚本约定,参数校验层至今没有对应断言。
- fast 测试 fixtures 缺 update_weight_transfer_mode 导致既有测试失败 (testing): 两处 fixture 补 update_weight_transfer_mode="broadcast",与 parser 默认值对齐。
- max_concurrency 提升与 FT concurrency group 的关系 (performance): 保留 elif args.update_weight_transfer_mode == "rdt" 分支,作者回复 'kept the concurrency setup as-is'。
- --use-rdt-weight-sync 别名与 --update-weight-transfer-mode 重复 (design): 作者回复 'removed',最终只保留 --update-weight-transfer-mode 的 rdt 枚举值作为唯一开关。
风险与影响
- 风险:
- 兼容面被显著收窄:
miles/utils/arguments.py 强制 rdt ⊕ colocate / PPO / FSDP / PD / LoRA 互斥,用户必须重新规划 GPU 池(e2e 即用 6 actor + 2 rollout 的非 colocate 布局),误配只能靠断言拦下。
- 参数校验留白:review 明确提出的“校验 rollout 后端是 Ray 而非 mp”没有落到
miles_validate_args,只在运行脚本层面约定,配置错误可能延后到运行时才暴露。
- 异质 engine 布局被硬拒绝:
connect_rollout_engines() 对 engine_gpu_counts 不一致直接 ValueError,--sglang-config 覆盖 num_gpus_per_engine 的用法在 rdt 下不可用,per-engine transfer plan 属明确推迟项。
- 私有/实验 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 已上调),升级或回退会双向耦合。
- 全局状态与 monkey patch:
create_gpu_replica() 期间替换 sglang.srt.model_loader.loader.post_load_weights 模块级函数并调用 set_global_server_args_for_scheduler(),虽有 try/finally 恢复,但对并发/异常路径不具线程安全性;NIXL 注册终身有效也意味着 GPU 内存不可回收。
- 生命周期与容错:node_rank != 0 不再持有 process,
shutdown() 依赖 ray.kill(self._sglang_server_actor) 连带清理子任务中的 SchedulerActor;yueming-yuan 的 FT/清理顾虑只被部分回应,e2e 之外没有覆盖故障场景。
- 测试覆盖不均:bucket 打包对齐(
_staging_span)、分轮 flush 与 _EngineRankBucket.stage 只有 est_time=700 的 8 卡 e2e 与参数级 fast 测试保护,缺少可直接跑在 fast-gpu 上的一致性单测。
- 文档与实现不一致: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 单测缺失(打包与分轮逻辑), 进程式生命周期契约变更
关联脉络
- PR #27723 [sglang-miles] RDT/NIXL weight sync support for Ray scheduler actors: sglang 侧配对实现,提供 SchedulerActor.pull_weights、RayEngine named actors、enable_engine_info_bootstrap 与 ray>=2.55.1,本 PR 的 _init_normal 直接依赖这些开关。
- PR #34621 [ray] expose get_scheduler_actor_name so callers can look up the schedulers: 同一依赖链上的上游改动,把 SchedulerActor 名称格式(_pg{hex} → _port{port})暴露给外部调用方,对应本 PR 早期按名字定位调度器的实现。
- PR #3128 fix: use host weights when syncing an offloaded actor: 同属权重同步主线的近期修复,改动同样落在 miles/backends/megatron_utils/actor.py 与 PPO 生命周期测试上,可与 RDT 的 actor 侧装配点对照阅读。
- PR #3217 Reduce NVFP4 weight synchronization overhead: 同为降低权重同步开销的近期工作(消除主机同步与规则扫描),说明仓库正在多条并行路线上优化 trainer→rollout 的同步成本,RDT 是其中的传输层方案。
- PR #2701 feat(ci): declare supported GPU generations on every CUDA test: 新 e2e 测试按仓库约定使用 register_cuda_ci 注册 suite 与 labels,与该 CI 注册/硬件声明机制属同一套测试基建脉络,新增测试需要与之保持一致。
参与讨论