Prhub

#20260 Fix port overflow in DP attention path when base port is near 65535

原始 PR 作者 janbernloehr 合并时间 2026-06-08 15:39 文件变更 2 提交数 1 评论 19 代码增减 +33 / -8

执行摘要

修复 DP attention 路径端口溢出当基础端口接近 65535

Issue #20257 报告了在 SLURM 环境下使用 --enable-dp-attention 且 master 端口接近 65535 时,派生端口(如 65536)非法导致 AttributeError: 'NoneType' object has no attribute 'pid'。PR body 指出这是 PR #2826 对非 DP 路径修复的跟进,但 DP attention 路径仍使用 TCP 端口算术偏移且无边界检查。

该 PR 修复了实际用户报告的启动崩溃问题,设计上兼顾了多节点确定性要求,且对非溢出场景零影响。建议合并,并考虑后续为端口分配逻辑补充单元测试。

讨论亮点
  • Off-by-one 判断:gemini-code-assist[bot] 指出原条件 dist_init_port + 1 + NUM_DERIVED_PORTS > 65535 过于保守(如 dist_init_port=65530 时最高端口仍合法),作者已修正为 dist_init_port + NUM_DERIVED_PORTS > 65535
  • LUT / 随机端口提议:alexnails 建议改用查找表或随机端口分配避免溢出,但作者回应多节点场景下端口推导必须确定性(所有节点独立计算相同端口),随机方案不可行。
  • 端口上限隐含限制:alexnails 指出单节点递减逻辑隐含 server_args.port ≤ 65530 的硬性要求,作者解释该递减分支正是为支持 SLURM 分配的近上限端口而设,接受此限制。

实现拆解

  1. python/sglang/srt/utils/network.py:增加 MAX_VALID_PORT 常量(65535),在 wait_port_available() 入口对端口范围进行前置校验,非法端口直接抛出 ValueError;将 error_message 初始化为兜底字符串避免 UnboundLocalError;修复 find_process_using_port() 返回 None 时仍访问 process.pid 的 bug,将相关逻辑移入 else 分支。
  2. python/sglang/srt/server_args.py单节点 DP:将 server_args.port + ZMQ_TCP_PORT_DELTA 的计算改为先计算 derived_port,若超过 65535 则改用 server_args.port - ZMQ_TCP_PORT_DELTA,保证派生端口合法。
  3. python/sglang/srt/server_args.py多节点 DP:定义 NUM_DERIVED_PORTS = 5,检查 dist_init_port + NUM_DERIVED_PORTS > 65535,若超限则将 port_base 设为 dist_init_port - NUM_DERIVED_PORTS - 1,否则沿用递增逻辑。推导公式保证所有节点无通信即可得出一致端口。
文件 模块 状态 重要度
python/sglang/srt/utils/network.py 网络工具 modified 6.34
python/sglang/srt/server_args.py 服务参数 modified 6.12

关键符号

wait_port_available PortArgs.init_new

关键源码片段

python/sglang/srt/utils/network.py core-logic

实现了端口范围校验和异常处理修复,是防御性编程的核心文件。

# python/sglang/srt/utils/network.pyMAX_VALID_PORT = 65535 # TCP 端口上限def wait_port_available(
    port: int, port_name: str, timeout_s: int = 30, raise_exception: bool = True
) -> bool:
    # 前置校验:拒绝非法端口,给出清晰错误而非后续的 None 属性异常
    if port < 0 or port > MAX_VALID_PORT:
        raise ValueError(
            f"{port_name} has invalid port number {port}. "
            f"Valid TCP port range is 0-{MAX_VALID_PORT}."
        )
​
    # 提前初始化兜底消息,避免超时未进入 logging 分支时 UnboundLocalError
    error_message = f"{port_name} at {port} is not available"
    for i in range(timeout_s):
        if is_port_available(port):
            return True
​
        if i > 10 and i % 5 == 0:
            process = find_process_using_port(port)
            if process is None:
                logger.warning(
                    f"The port {port} is in use, but we could not find "
                    f"the process that uses it."
                )
            else:
                # 原 bug:process 为 None 时仍执行此分支,引发 AttributeError
                pid = process.pid
                error_message = (
                    f"{port_name} is used by a process already. "
                    f"{process.name()=}' {process.cmdline()=} "
                    f"{process.status()=} {pid=}"
                )
                logger.info(
                    f"port {port} is in use. Waiting for {i} seconds "
                    f"for {port_name} to be available. {error_message}"
                )
        time.sleep(0.1)
​
    if raise_exception:
        raise ValueError(
            f"{port_name} at {port} is not available in {timeout_s} seconds. "
            f"{error_message}"
        )
    return False
python/sglang/srt/server_args.py core-logic

DP attention 端口派生逻辑所在,包含单节点和多节点的溢出规避核心变更。

# python/sglang/srt/server_args.py (PortArgs.init_new 方法中 DP 分支 )
else:
    # DP attention. 使用 TCP 端口以支持单节点与多节点
    if server_args.nnodes == 1 and server_args.dist_init_addr is None:
        # 单节点:基于 server_args.port 派生
        derived_port = server_args.port + ZMQ_TCP_PORT_DELTA
        if derived_port > 65535:
            # 与 PR #2826 相同的递减策略,避免端口溢出
            derived_port = server_args.port - ZMQ_TCP_PORT_DELTA
        na = NetworkAddress("127.0.0.1", derived_port)
    else:
        na = NetworkAddress.parse(server_args.dist_init_addr)
​
    dist_init_host = na.host
    dist_init_port = na.port
​
    # 需要连续 5 个端口:port_base, detokenizer, rpc, metrics, scheduler
    # 多节点下必须确定性推导(不可搜索可用端口)
    NUM_DERIVED_PORTS = 5
    if dist_init_port + NUM_DERIVED_PORTS > 65535:
        # 超限则递减,确保所有派生端口 ≤ 65535
        port_base = dist_init_port - NUM_DERIVED_PORTS - 1
    else:
        port_base = dist_init_port + 1
​
    detokenizer_port = port_base + 1
    rpc_port = port_base + 2
    metrics_port = port_base + 3
    load_collector_port = port_base + 5
    # ... 其余端口赋值及 wait_port_available 调用不变

评论区精华

Off-by-one 范围判断 正确性

gemini-code-assist[bot] 指出原条件 `dist_init_port + 1 + NUM_DERIVED_PORTS > 65535` 过于保守,例如 `dist_init_port=65530` 时最高端口 65535 合法但被拒。

结论:作者将条件修正为 `dist_init_port + NUM_DERIVED_PORTS > 65535`,已合入代码。 · 已解决

使用随机端口替代固定偏移 设计

alexnails 建议放弃固定偏移,改用查找表随机选择 5 个端口以避免溢出。

结论:作者解释多节点场景下所有节点必须独立推导出相同端口,随机分配需要通信协调,与当前无通信的服务发现模型冲突,因此保留确定性递减。 · 已解决

单节点递减隐含端口上限限制 question

alexnails 指出单节点递减逻辑隐含 `server_args.port ≤ 65530` 的硬性要求。

结论:作者承认此限制,但强调递减分支是为支持 SLURM 分配的近上限端口而设,若用户端口恰好为 65535 则减法仍有效(65535-200=65335),实际无额外风险。 · 已解决

风险与影响

  • 端口冲突风险:递减策略可能导致派生端口与系统预留端口(如 0-1023)重叠,但 dist_init_port 通常为高位端口(如 SLURM 分配),冲突概率低。
  • 缺少测试覆盖:未增加单元测试验证端口溢出路径,回归风险虽低但存在。
  • 单节点递减假设:单节点 DP 中假设 server_args.port - ZMQ_TCP_PORT_DELTA 仍为正数,若 server_args.port 接近 0 可能下溢,但实际场景中用户端口通常较大。
  • 用户:使用 DP attention 且基础端口接近 65535(常见于 SLURM 环境)的用户将不再启动崩溃。
  • 系统:端口分配逻辑变更仅影响启动阶段,不影响推理性能。
  • 团队:变更集中在两个源文件共 33 行增量,影响面小。
缺少测试覆盖 启动流程变更

关联 Issue

#20257 [Bug] Port overflow in DP attention path when base port is near 65535

完整报告

参与讨论