# PR #2286 完整报告

- 仓库：`THUDM/slime`
- 标题：fix: improve compatibility with older SGLang versions
- 合并时间：2026-08-20 15:01
- 原文链接：http://prhub.com.cn/THUDM/slime/pull/2286

---

# 执行摘要

- 一句话：修复旧版 SGLang 参数别名与 router 选项兼容性
- 推荐动作：值得快速阅读的小型兼容性修复。核心可借鉴点是：跨版本依赖适配时用 `hasattr` 做能力探测并降级为 warning，而不是硬假设 API 恒存在。同时建议关注两个遗留点：`validate_args` 在跳过别名归一化后仍直接访问 `args.sglang_pp_size` 的潜在 `AttributeError`，以及缺失 `disable_health_check` 时 router 健康检查语义变化；若团队需长期支持旧版 SGLang，应补充一个 mock 旧版属性的回归测试。

# 功能与动机

PR body 明确说明动机：SGLang argument and router APIs vary between releases. Slime previously assumed that at least one name in every argument alias pair and the disable_health_check router option were always available. With releases that do not provide those attributes, validation or router startup could fail before rollout initialization. 即 Slime 原实现把 SGLang 的 API 视为恒等不变，旧版本一旦缺少别名或 router 选项，就会在 rollout 初始化前崩溃。此外，head 分支名 dev/musa-patch-only 暗示该修复服务于特定硬件平台（MUSA）的适配，但 PR body 未对平台背景展开说明。

# 实现拆解

本次变更沿两条路径展开，核心手法一致：先做能力探测，再决定是否赋值。

1. **参数别名归一化容错（`slime/backends/sglang_utils/arguments.py`）**
 - 原实现用一行三元表达式取值：`getattr(args, current_name) if hasattr(args, current_name) else getattr(args, legacy_name)`。当旧版 SGLang 同时不注册短名与长名时，`getattr` 直接抛 `AttributeError`，参数校验在 rollout 初始化前崩溃。
 - 新实现改为 `if / elif / else` 三分支：短名优先、长名次之、二者均缺失时记录 `logger.warning` 并 `continue` 跳过归一化；同时新增 `import logging` 与模块级 `logger`。
 - 影响范围仅限 4 组并行参数别名对（`sglang_dp_size`、`sglang_pp_size`、`sglang_ep_size`、`sglang_moe_dp_size`），对属性已注册的版本执行路径不变。

2. **router 选项探测（`slime/ray/rollout.py`）**
 - `_start_router` 构建 `RouterArgs` 后原本无条件设置 `router_args.disable_health_check = True`；旧版 `sglang_router.launch_router.RouterArgs` 未定义该字段时，原生 Router 构造会因为收到不支持的动态属性而失败。
 - 新逻辑用 `hasattr(router_args, "disable_health_check")` 探测，存在才置位，缺失时打印 warning 继续；该改动位于端口分配与 prometheus 端口设置之后，能保证 router 进程顺利拉起并进入存活检查。

3. **验证方式**
 - 作者运行 `pytest -q tests/utils/test_sglang_arguments.py tests/test_megatron_argument_validation.py`，20 项测试全部通过。
 - 本次未新增或修改任何测试文件，" 旧版 SGLang 完全缺失属性 " 的极端场景尚无自动化用例覆盖。

关键文件：
- `slime/backends/sglang_utils/arguments.py`（模块 SGLang 后端；类别 source；类型 dependency-wiring；符号 validate_args）: 参数别名归一化逻辑的核心改动点。原实现两个名字都缺时抛 AttributeError，新实现跳过并 warning，避免旧版 SGLang 在参数校验阶段启动失败。
- `slime/ray/rollout.py`（模块 路由启动；类别 source；类型 core-logic；符号 _start_router）: router 启动路径，设置 disable_health_check 前增加 hasattr 探测，防止旧版 RouterArgs 不支持该字段时启动失败。

关键符号：validate_args, _start_router

## 关键源码片段

### `slime/backends/sglang_utils/arguments.py`

参数别名归一化逻辑的核心改动点。原实现两个名字都缺时抛 AttributeError，新实现跳过并 warning，避免旧版 SGLang 在参数校验阶段启动失败。

```python
def validate_args(args):
    # SGLang 各版本对并行参数的 argparse dest 命名不一致：
    # 新版本使用短名（如 sglang_dp_size），旧版本使用长名（如 sglang_data_parallel_size）。
    # 这里同时保留两个属性，优先采用新版本短名；若两个名字都未注册（旧版本 API 差异），
    # 则跳过归一化并记录 warning，避免在 rollout 初始化前直接抛 AttributeError。
    for current_name, legacy_name in (
        ("sglang_dp_size", "sglang_data_parallel_size"),
        ("sglang_pp_size", "sglang_pipeline_parallel_size"),
        ("sglang_ep_size", "sglang_expert_parallel_size"),
        ("sglang_moe_dp_size", "sglang_moe_data_parallel_size"),
    ):
        if hasattr(args, current_name):
            value = getattr(args, current_name)
        elif hasattr(args, legacy_name):
            value = getattr(args, legacy_name)
        else:
            logger.warning(
                "The installed SGLang registered neither %s nor %s; "
                "skipping compatibility alias normalization for this parameter.",
                current_name,
                legacy_name,
            )
            continue
        setattr(args, current_name, value)
        setattr(args, legacy_name, value)

    # 注意：若上面跳过了 sglang_pp_size / sglang_dp_size，此处直接访问属性可能仍会抛错；
    # 由于参数由 Slime 的 add_sglang_arguments 统一注册，实际场景中两个名字通常至少存在一个。
    if args.sglang_pp_size > 1:
        assert args.rollout_num_gpus_per_engine % args.sglang_pp_size == 0, (
            f"rollout_num_gpus_per_engine ({args.rollout_num_gpus_per_engine}) must be divisible by "
            f"sglang_pipeline_parallel_size ({args.sglang_pp_size})"
        )
        args.sglang_tp_size = args.rollout_num_gpus_per_engine // args.sglang_pp_size
    else:
        args.sglang_tp_size = args.rollout_num_gpus_per_engine

    if args.sglang_dp_size > 1:
        assert args.sglang_enable_dp_attention

```

### `slime/ray/rollout.py`

router 启动路径，设置 disable_health_check 前增加 hasattr 探测，防止旧版 RouterArgs 不支持该字段时启动失败。

```python
def _start_router(args, *, has_pd_disaggregation: bool = False, force_new: bool = False):
    # 复用已有 router 地址；否则在本机分配端口并启动新 router 进程。
    if not force_new and args.sglang_router_ip is not None:
        return args.sglang_router_ip, args.sglang_router_port

    router_ip = _wrap_ipv6(get_host_info()[1])
    if force_new:
        router_port = find_available_port(random.randint(3000, 4000))
    else:
        router_port = args.sglang_router_port
        if router_port is None:
            router_port = find_available_port(random.randint(3000, 4000))

    from sglang_router.launch_router import RouterArgs
    from slime.utils.http_utils import run_router

    router_args = RouterArgs.from_cli_args(args, use_router_prefix=True)
    router_args.host = router_ip
    router_args.port = router_port
    router_args.prometheus_port = find_available_port(random.randint(4000, 5000))
    router_args.request_timeout_secs = args.sglang_router_request_timeout_secs

    if has_pd_disaggregation:
        router_args.pd_disaggregation = True

    # 关闭熔断器，避免 RDMA 传输超时（PCIe 争用下的瞬时抖动）被误判为 decode worker 宕机。
    router_args.disable_circuit_breaker = True

    # Slime 不使用 router 自带的健康检查；旧版 SGLang router 可能未暴露该字段，
    # 直接赋值会导致旧版本启动失败，因此先探测属性是否存在，缺失时降级为 warning。
    if hasattr(router_args, "disable_health_check"):
        router_args.disable_health_check = True
    else:
        logger.warning("The installed SGLang does not provide the disable_health_check parameter.")

    logger.info(f"Launch router with args: {router_args}")

    process = multiprocessing.Process(target=run_router, args=(router_args,))
    process.daemon = True
    process.start()
    time.sleep(3)
    assert process.is_alive()
    logger.info(f"Router launched at {router_ip}:{router_port}, Prometheus port: {router_args.prometheus_port}")
    return router_ip, router_port

```

# 评论区精华

本 PR 无任何 review 评论或讨论线程（comments_count=0、review_comments_count=0），方案由作者在 PR body 中单方面说明，随后由 zhuzilin 直接合并，没有可提炼的设计交锋。值得留意的唯一 " 对话缺口 " 是：作者在 Testing 节给出的是手工 pytest 命令（20 passed），但没有为 " 旧版 SGLang 完全缺少属性 / 选项 " 的场景新增 mock 测试，对跨版本兼容修复而言缺少自动化回归保护。

- 暂无高价值评论线程

# 风险与影响

- 风险：
 1. **参数归一化跳过后的下游访问风险**：`validate_args` 中若某组别名对两个名字都缺失，`continue` 会跳过 `setattr`，但紧随其后的 `args.sglang_pp_size > 1`、`args.sglang_dp_size > 1` 仍直接访问属性，属性真缺失时依然抛 `AttributeError`。实际场景中 Slime 的 `add_sglang_arguments` 会统一注册这些参数，两者同时缺失的概率较低，但防御并不完整。
 2. **router 健康检查语义变化**：`disable_health_check` 不可用时，router 会启用自身健康检查；而代码注释明确表示 Slime 不希望使用 router 健康检查（RDMA 瞬断可能误判 worker 死亡），旧版上该语义会静默失效，网络抖动场景需额外观察。
 3. **测试覆盖缺口**：未新增针对属性缺失场景的回归测试，现有 20 项测试无法证明该修复在真实旧版 SGLang 上生效。
 4. **依赖版本漂移**：`hasattr` 探测能容忍小版本差异，但若 SGLang 未来改变属性命名策略，仍可能出现新的不匹配。
 - 影响：**用户侧**：使用旧版 SGLang（不暴露并行别名或 `disable_health_check`）的用户可越过参数校验与 router 启动两道关卡，正常进入 rollout 初始化；新版 SGLang 用户行为完全不变。
**系统侧**：启动路径从 " 强假设 " 变为 " 能力探测 "，鲁棒性提升；health check 语义在旧版上的变化需要运维侧留意。
**团队侧**：`dev/musa-patch-only` 分支名暗示本 PR 处于 MUSA 平台适配链条中，与既有 ROCm、NPU 适配共同构成平台兼容矩阵。
**影响程度**：改动面窄（2 个文件、19 行），不涉及训练、损失、数据路径，风险集中在 rollout 启动阶段。

- 风险标记：依赖版本兼容 , 参数缺失时下游 AttributeError, router 健康检查行为变化 , 缺少新增测试覆盖

# 关联脉络

- PR #2236 fix: don't overwrite an explicitly set --start-rollout-id: 同类参数校验问题：防止 argparse 属性在版本演进中被覆写 / 丢失，与本 PR 的别名归一化同属 arguments 兼容性域。
- PR #2276 Add args check for --save-debug-train-data: 同为参数校验增强，且共用 tests/test_megatron_argument_validation.py 测试面。
- PR #2261 fix(rollout): restore partial continuation token budget: 同为 SGLang rollout 路径的兼容性修复，与本 PR 属于同一功能线。
- PR #2199 fix(npu): bracket IPv6 hosts in distributed init methods: 同为平台兼容性修复；arguments.py 与 rollout.py 均涉及 _wrap_ipv6 调用，且 head 分支名 dev/musa-patch-only 表明处于平台适配链条。