Prhub

#26114 [PD] Fix IB device validation for JSON mappings

原始 PR 作者 stmatengss 合并时间 2026-05-29 16:44 文件变更 4 提交数 8 评论 13 代码增减 +182 / -81

执行摘要

修复 IB 设备 JSON 映射验证回归

disaggregation_ib_devicemooncake_ib_device 支持 per-GPU JSON 映射格式后,_validate_ib_devices 未能正确接受这些格式,导致 PD 分解和弹性 EP 功能回归。PR body 明确指出要 'accept all documented input forms that were already supported downstream'。

该 PR 属于重要的 bugfix + 小重构,建议阅读以了解 IB 设备配置的设计模式和验证流程。尤其关注 parse_ib_device_config 的提取和内部函数 _normalize_device_group 的复用方式,对类似配置解析场景有参考价值。

讨论亮点
  • 核心讨论:reviewer ShangmingCai 指出 parse_ib_device_config 未处理 json.JSONDecodeError,作者随后补充了异常处理。
  • 重要建议:reviewer 发现 model_runner.py 第 1053 行仍使用 .split(","),建议修复以支持 JSON 映射,作者通过 Copilot 提交了修复。
  • 决策结论:UNIDY2002 确认 model_runner.py 的修复 LGTM。
  • 未解决疑虑:无。

实现拆解

  1. 提取公共解析函数:在 mooncake_transfer_engine.py 中新增 parse_ib_device_config,将原 get_ib_devices_for_gpu 的 JSON/文件解析逻辑独立出来,返回 Union[str, Dict[int, str]],并添加了更严格的类型校验和异常处理。
  2. 增强验证函数:在 server_args.py_validate_ib_devices 中新增内部函数 _normalize_device_group,复用 parse_ib_device_config 解析输入,并通过 sysfs 验证设备名称合法性,支持逗号分隔字符串、JSON 对象和 JSON 文件三种格式。
  3. 修复弹性 EP 后端:在 model_runner.pyinit_torch_distributed 中,将原 mooncake_ib_device.split(",") 替换为 get_ib_devices_for_gpu(mooncake_ib_device, self.gpu_id),使 per-GPU JSON 映射能被正确解析到当前 GPU。
  4. 补充测试覆盖:在 test_server_args_backend.py 中新增 TestServerArgsIBDeviceValidation 测试类,包含三个用例:逗号分隔字符串、JSON 对象、JSON 文件路径,通过 mock sysfs 设备列表来验证。
文件 模块 状态 重要度
python/sglang/srt/distributed/device_communicators/mooncake_transfer_engine.py 通信层 modified 7.93
python/sglang/srt/server_args.py 启动配置 modified 7.48
test/registered/cpu/test_server_args_backend.py 测试 modified 6.75
python/sglang/srt/model_executor/model_runner.py 模型运行 modified 5.92

关键符号

parse_ib_device_config get_ib_devices_for_gpu _validate_ib_devices _normalize_device_group

关键源码片段

python/sglang/srt/distributed/device_communicators/mooncake_transfer_engine.py refactor

核心解析逻辑提取为 `parse_ib_device_config`,供其他模块复用;`get_ib_devices_for_gpu` 重构为调用该函数。

def parse_ib_device_config(
    ib_device_str: Optional[str],
) -> Optional[Union[str, Dict[int, str]]]:
    """Parse IB device config from a shared string, JSON mapping, or JSON file."""
    if ib_device_str is None or not ib_device_str.strip():
        return None
​
    normalized_input = ib_device_str.strip()
    # Quick reject: treat as plain comma-separated string if not JSON-like
    if not normalized_input.endswith(".json") and not normalized_input.startswith("{"):
        return normalized_input
​
    if normalized_input.endswith(".json"):
        if not os.path.isfile(normalized_input):
            raise RuntimeError(f"File {normalized_input} does not exist.")
        try:
            with open(normalized_input, "r", encoding="utf-8") as file:
                mapping = json.load(file)
        except json.JSONDecodeError as exc:
            raise RuntimeError(
                f"Failed to parse JSON content from file {normalized_input}"
            ) from exc
        except (IOError, OSError) as exc:
            raise RuntimeError(
                f"Failed to read JSON file {normalized_input}: {exc}"
            ) from exc
    else:
        try:
            mapping = json.loads(normalized_input)
        except json.JSONDecodeError as exc:
            raise ValueError(f"Invalid JSON mapping: {normalized_input}") from exc
​
    if not isinstance(mapping, dict):
        raise ValueError(
            "Invalid format: expected a mapping from GPU id to IB device string"
        )
​
    normalized_mapping: Dict[int, str] = {}
    for gpu_key, ib_devices in mapping.items():
        # Accept both int keys and string-digit keys
        normalized_key = int(gpu_key) if str(gpu_key).isdigit() else None
        if normalized_key is None or not isinstance(ib_devices, str):
            raise ValueError(
                "Invalid format: keys must be integers (or string "
                "representations of integers) and values must be strings"
            )
        normalized_mapping[normalized_key] = ib_devices.strip()
​
    if not normalized_mapping:
        raise ValueError("No valid GPU mappings found in JSON")
​
    return normalized_mapping

评论区精华

parse_ib_device_config 缺少 JSONDecodeError 处理 正确性

ShangmingCai 指出 parse_ib_device_config 未处理 json.JSONDecodeError 异常。

结论:作者补充了 except json.JSONDecodeError 分支,抛出 RuntimeError。 · 已解决

model_runner.py 需同步修复以支持 JSON 映射 正确性

ShangmingCai 发现 model_runner.py 仍使用 .split(","),要求修复以支持 JSON 映射,并提及 @UNIDY2002 审查。

结论:作者通过 Copilot 提交修复,使用 get_ib_devices_for_gpu,UNIDY2002 确认 LGTM。 · 已解决

新增单元测试覆盖 测试

stmatengss 在评论中说明已添加注册单元测试以确保功能不被修改。

结论:新增 TestServerArgsIBDeviceValidation 测试类,包含三个测试方法。 · 已解决

风险与影响

  • 核心路径变更:_validate_ib_devices 在启动时调用,不影响推理热路径,风险低。
  • 兼容性:向后兼容,旧格式逗号分隔字符串仍受支持(由 parse_ib_device_config 快速路径直接返回)。
  • 测试覆盖:新增了 CPU 回归测试,覆盖三种格式,且在 CI 中成功运行。
  • 异常处理:JSON 解析和文件读取失败均抛出明确的 RuntimeErrorValueError,但未覆盖空文件或非 dict JSON 的边界场景。
  • 用户:PD 分解和弹性 EP 用户现在可以使用 JSON 映射或 JSON 文件配置 IB 设备,无需再使用逗号分隔格式。
  • 系统:不影响模型推理逻辑,仅影响初始化阶段的设备验证和分布式后端设置。
  • 团队:通过提取公共解析函数,降低了 server_args.pymodel_runner.py 的维护成本,使 IB 设备配置逻辑集中化。
配置验证变更 启动路径 JSON 解析边界未覆盖 缺少空文件测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论