Prhub

#56687 [ROCm][Bugfix] Initialize Ray NIXL agents for sharded RDT

原始 PR 作者 AndreasKaratzas 合并时间 2026-09-15 08:34 文件变更 5 提交数 4 评论 9 代码增减 +144 / -7

执行摘要

为 sharded RDT 打通 ROCm 下 Ray NIXL agent 初始化

PR body 明确指出:#50922 扩展 AMD CI 覆盖后,最终 sharded RDT 示例在 Buildkite build 88592 首次尝试和重试中都失败;根因是 Ray 2.56.1 创建 NIXL agent 时执行 from nixl._api import ...,而 ROCm 安装只提供 nixl_rocm._api,属于传输集成缺口而非小 DeepSeek 模型回归。因此需要在 Ray 注册 NIXL 内存前,让 Ray 的私有导入在 ROCm 上能解析到正确的 agent 构造器。

值得精读。核心价值不在代码量,而在设计决策:用模块别名代替复制上游逻辑,把平台差异收敛到一个最小入口;测试用 ModuleType + Mock 伪造平台包、通过记录导入时刻环境变量来验证配置时序的手法也很有参考意义。

讨论亮点

核心讨论来自审核者 aoshen02 对初版实现的设计批评:初版 initialize_ray_nixl 逐行复刻了 Ray 的 get_nixl_agent(backends=["UCX"]、actor-id 命名、RAY-DRIVER fallback)并直接写私有字段 transport._nixl_agent,这意味着未来 Ray 内部配置一旦变化,ROCm 路径会静默偏离。aoshen02 指出 from nixl._api import ... 是 ray/experimental/rdt/ 中唯一硬编码的 NIXL 导入,所有 7 个 get_nixl_agent() 调用者都经由它,因此只要重定向模块名就能让 Ray 自己构建正确 agent。作者随后采纳该方案,改为模块别名 + 统一 wrapper,并精简了原本断言 Ray 内部配置与描述符格式的测试,同时补充了 UCX 环境变量必须在首次导入前设置的时序测试。此外 CI 流程中首次 /ci run 因分支落后 main 84 commits 被拒,merge main 后 Buildkite CI #88934 正常触发。

实现拆解

  1. 新增模块别名入口:在 vllm/distributed/nixl_utils.py 增加 alias_nixl_for_ray()。非 ROCm 平台或 nixl._api 已存在于 sys.modules 时直接返回;否则先通过 is_nixl_available() 校验平台 NIXL 包是否存在(缺失则抛 ImportError),在首次导入前调用 _maybe_set_ucx_rcache_limit() 设置 UCX rcache 上限,再导入 ROCm 包 nixl_rocm 与 nixl_rocm._api,用 sys.modules.setdefault 把 nixl、nixl._api 两个名字指到 ROCm 实现。这样 Ray 内部导入时拿到的是 ROCm agent 构造器,而 agent 的构建、配置、缓存仍完全由 Ray 负责。
  2. 收敛内存注册入口:在 vllm/distributed/weight_transfer/sharded_rdt_common.py 新增 register_nixl_memory(),先调用 alias_nixl_for_ray(),再延迟导入 ray.experimental.register_nixl_memory 并转发。该 wrapper 成为 sharded RDT 所有 NIXL 内存注册的唯一入口,避免各调用点重复处理平台差异。
  3. 替换 4 个调用点:sharded_rdt_engine.py 的 _preregister_at_init()(consumer 接收槽预注册)与 _issue_pull()(运行期槽位扩容),以及 sharded_rdt_trainer.py 的 warmup_nixl()(启动时 agent 预热)与 _new_serve_buffer()(producer serve 槽位分配),都从直连 from ray.experimental import register_nixl_memory 改为使用共享 wrapper;_issue_pull 仅保留 set_target_for_ref 直连。
  4. 测试配套:tests/distributed/test_sharded_rdt_producer.py 新增 ray_nixl_transport fixture,用 ModuleType + Mock 伪造 nixl_rocm 包,覆盖:ROCm alias 下 register_nixl_memory 能到达 Ray agent 且 alias 幂等;UCX 环境变量(默认 1024 或用户覆盖)在首次导入前生效;非 ROCm 或已存在 nixl._api 时不改动;ROCm NIXL 包缺失时抛 ImportError 且不污染 sys.modules。
文件 模块 状态 重要度
vllm/distributed/nixl_utils.py NIXL 适配 modified 6.77
vllm/distributed/weight_transfer/sharded_rdt_common.py RDT 公共 modified 6.14
vllm/distributed/weight_transfer/sharded_rdt_engine.py RDT 引擎 modified 4.94
vllm/distributed/weight_transfer/sharded_rdt_trainer.py RDT 训练 modified 4.73
tests/distributed/test_sharded_rdt_producer.py RDT 测试 modified 6.97

关键符号

alias_nixl_for_ray register_nixl_memory warmup_nixl _new_serve_buffer _preregister_at_init _issue_pull

关键源码片段

vllm/distributed/nixl_utils.py core-logic

新增 `alias_nixl_for_ray()`,通过 `sys.modules` 别名把 Ray 的 `nixl._api` 导入重定向到 ROCm 实现,是本 PR 的核心修复逻辑。

def alias_nixl_for_ray() -> None:
    """让 Ray 的 ``nixl._api`` 导入解析到 ROCm 实现。
​
    设计要点:只做“导入别名”,不复制 Ray 的 agent 创建逻辑。
    Ray 内部会自行 import ``nixl._api`` 并据此构建 agent,
    我们只需在首次导入前把 ``nixl`` / ``nixl._api`` 两个模块名
    指到 ROCm 的包即可,后续行为由 Ray 自身保证。
    """
    # 非 ROCm 平台或 Ray 的 NIXL 已被真实导入时直接跳过:
    # 前者保持原路径不变,后者说明 Ray 的 agent 体系已可用,
    # 继续做 alias 反而可能覆盖真实模块。
    if not current_platform.is_rocm() or "nixl._api" in sys.modules:
        return
​
    package_name = _get_nixl_package_name()
    if not is_nixl_available():
        raise ImportError(f"Ray's NIXL transport on ROCm requires {package_name}")
​
    # UCX 的 rcache 上限必须在 NIXL 底层库首次导入前设置,
    # 否则该环境变量不会再被读取,RDMA 注册缓存会走默认值。
    _maybe_set_ucx_rcache_limit()
    package = importlib.import_module(package_name)
    api = importlib.import_module(f"{package_name}._api")
    # setdefault:若其他库已注册同名模块,则保留既有实现,
    # 避免破坏可能已经创建的 agent。
    sys.modules.setdefault("nixl", package)
    sys.modules.setdefault("nixl._api", api)
tests/distributed/test_sharded_rdt_producer.py test-coverage

新增 fixture 与 4 个用例,验证 alias 注册链路、UCX 时序、平台门控与缺失包报错,是本 PR 行为契约的主要保障。

@pytest.fixture
def ray_nixl_transport(monkeypatch):
    """伪造一个“ROCm NIXL 包”,用来验证 alias 与注册链路。
​
    测试思路:
    1. 用 ModuleType + Mock 构造 nixl_rocm / nixl_rocm._api,不依赖真实驱动;
    2. 把 nixl_utils.importlib.import_module 替换为记录型 Mock,
       从而断言“UCX 环境变量在首次导入前已生效”这一关键时序;
    3. 测试结束后手工清理 sys.modules 与环境变量,防止污染同进程的后续用例。
    """
    import ray
    from ray.experimental.rdt import util
    from ray.experimental.rdt.nixl_tensor_transport import NixlTensorTransport
    from vllm.distributed import nixl_utils
    from vllm.platforms import current_platform
​
    transport = NixlTensorTransport()
    package = ModuleType("nixl_rocm")
    api = ModuleType("nixl_rocm._api")
    api.__dict__.update(nixl_agent=Mock(), nixl_agent_config=Mock())
    package.__dict__["_api"] = api
    modules = {package.__name__: package, api.__name__: api}
    imported_rcache_limits = []
​
    def import_nixl(name):
        # 记录每次导入时 UCX 环境变量的值,用于验证设置顺序。
        imported_rcache_limits.append(os.environ.get("UCX_RCACHE_MAX_UNRELEASED"))
        monkeypatch.setitem(sys.modules, name, modules[name])
        return modules[name]
​
    nixl_names = ("nixl", "nixl._api", *modules)
    for name in nixl_names:
        monkeypatch.delitem(sys.modules, name, raising=False)
    monkeypatch.delenv("UCX_RCACHE_MAX_UNRELEASED", raising=False)
    importer = Mock(side_effect=import_nixl)
    monkeypatch.setattr(
        nixl_utils, "importlib", SimpleNamespace(import_module=importer)
    )
    monkeypatch.setattr(nixl_utils, "is_nixl_available", lambda: True)
    monkeypatch.setattr(util, "transport_managers", {"NIXL": transport})
    monkeypatch.setattr(current_platform, "is_rocm", lambda: True)
    monkeypatch.setattr(
        ray,
        "get_runtime_context",
        lambda: SimpleNamespace(get_actor_id=lambda: "rdt-actor"),
    )
    try:
        yield transport, api, importer, imported_rcache_limits
    finally:
        # alias 直接写 sys.modules,必须手动清理,避免污染后续用例。
        for name in nixl_names:
            sys.modules.pop(name, None)
        os.environ.pop("UCX_RCACHE_MAX_UNRELEASED", None)

评论区精华

用模块别名替代克隆 Ray 的 agent 创建逻辑 设计

aoshen02 指出初版 `initialize_ray_nixl` 逐行复刻了 Ray 的 `get_nixl_agent`(`backends=["UCX"]`、actor-id 命名、`RAY-DRIVER` fallback)并写入私有 `transport._nixl_agent`,未来 Ray 配置一旦变化,ROCm 路径会静默分歧;而 `from nixl._api import ...` 是 `ray/experimental/rdt/` 中唯一硬编码的 NIXL 导入,重定向模块导入即可让 Ray 自己构建 agent。

结论:作者采纳建议,改为 `alias_nixl_for_ray()` + 共享 `register_nixl_memory()` wrapper,Ray 负责 agent 构建、配置与缓存。 · 已解决

测试精简:不再断言 Ray 内部机制 测试

作者在回复中说明已裁剪掉检查 Ray 内部配置与内存描述符格式的测试,保留对注册链路、alias 幂等、UCX 导入时序、平台门控与缺失包报错的回归覆盖。

结论:测试改为聚焦 vLLM 自身行为契约,避免绑定 Ray 内部实现细节。 · 已解决

CI 因分支落后 main 被拒 other

首次 `/ci run` 触发失败:PR 落后 upstream main 84 commits,Buildkite CI 要求先 merge 或 rebase 最新 main。

结论:作者合并 main 后重新触发,Buildkite CI #88934 正常启动。 · 已解决

风险与影响

  1. sys.modules 全局别名副作用:alias_nixl_for_ray() 直接改写进程级 sys.modules,一旦生效不可撤销;若未来 Ray 或其他库在同一个 worker 进程中并行管理自己的 nixl 模块,会产生隐式耦合。当前 4 个调用点均发生在首次 NIXL 使用前,风险可控,但单测只验证了清理逻辑,真实进程中的长期副作用未被覆盖。
  2. UCX 配置时序敏感:UCX_RCACHE_MAX_UNRELEASED 必须在 NIXL 底层库首次导入前设置,alias_nixl_for_ray() 假定调用时尚未有任何 NIXL 库被 import;若未来某依赖先导入了 nixl,setdefault 会静默保留旧模块,环境变量设置也会失效。测试通过记录导入时刻的环境变量值覆盖了这一时序。
  3. 依赖 Ray 私有导入路径:方案绑定 Ray 2.56.1 ray/experimental/rdt/ 中硬编码的 nixl._api 导入行为,Ray 后续版本若调整模块结构,alias 可能失效;但最坏情况会回到可见的 ImportError,不会静默出错。
  4. 测试仅 CPU 模拟:现有测试用 ModuleType 伪造 nixl_rocm,未覆盖真实 ROCm 硬件上的 RDMA 注册路径,端到端保障仍依赖 AMD CI。

对 ROCm 用户:启用 sharded_rdt 权重传输时不再因为 NIXL agent 初始化失败而崩溃,AMD 训练场景可用;对其他平台:alias_nixl_for_ray() 首行平台门控使非 ROCm 路径完全不变,回归面极小。对系统:NIXL agent 的构建、配置与缓存仍由 Ray 负责,vLLM 只做导入别名,行为与上游 Ray 保持一致。对团队:补齐了 #50922 新增 AMD CI 覆盖暴露的集成缺口,后续 ROCm + Ray RDT 组合有回归测试守护。

sys.modules 全局别名副作用 UCX 配置时序敏感 依赖 Ray 私有导入路径 测试仅 CPU 模拟

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论