Prhub

#37221 [Rust] Derive server address and accept signed env values

原始 PR 作者 merrymercy 合并时间 2026-09-01 03:30 文件变更 8 提交数 15 评论 11 代码增减 +119 / -59

执行摘要

Rust server 地址改为派生,env 解析支持负数

PR body 的 Motivation 原文:Make the embedded server launch boundary use the typed server configuration as its single address source, and match signed Python EnvInt values. This is PR 2 of 4 in stack #37223. 动机有二:一是此前 Python 侧 RustServer.launch 自行拼接 host/port 字符串传入 http_addr,地址逻辑双端各有一份容易漂移(如 IPv6 格式化、DP 偏移规则);二是 Python 的 EnvInt 接受负值(如 SGLANG_MAX_BATCH_REQS_PER_HTTP_REQ=-1 表示不设上限),而 Rust 的 env_u64 无法解析负数,导致同一配置在两种 server 模式下行为不一致。

值得精读,重点关注四个设计决策:①「配置派生 vs 调用方传入」——把地址计算收敛到 Rust 侧,启动边界只有一个事实来源;②env_i64 与 Python EnvInt 的符号对齐策略,以及 warn-and-default 的容错约定;③负数哨兵模式(-1 = 禁用限制)通过 batch_size_exceeds_limit 封装比较,避免 usize/i64 混用;④review 中作者自我否决 server.rs 命名的过程,体现模块命名的克制原则。

讨论亮点

review 仅两条评论,且都来自作者自己的自我审查,聚焦命名:

  • merrymercy(对初始文件名 server.rs):"do not name this file server.rs. give it an another name."
  • merrymercy(回复):"Renamed the helper module to utils/startup.rs and updated the import; the file now describes its startup-only role."
    要点是 server.rs 过于宽泛,会与主模块、Server 类概念混淆;startup.rs 精准表达其「仅服务启动边界」的定位。除此之外 rainj-me 直接批准,无功能争议。

实现拆解

  1. 新增启动辅助模块 utils/startup.rs:把 lib.rs 里的 value_error 迁入(保持 {context}: {err} 错误格式),新增核心函数 listen_addr(server_args, port_offset)——从 ServerArgs 读 host/port,用 checked_add 加 DP 偏移并对超过 65535 显式报错,再通过「先解析 server_args.bind() 字符串、后 set_port」的方式保留 IPv6 地址族。内置测试覆盖默认偏移、Some(7) 偏移、u16::MAX 溢出三个场景。
  2. Server::start 签名收敛(lib.rs:pyo3 构造器从 http_addr: Option<String> 改为 port_offset: Option<u16>,地址计算全部委托给 startup::listen_addr,错误统一走 value_error("bad listen address", e)std::net::SocketAddr 的直接 import 随之删除。
  3. 环境解析改带符号(utils/environ.rsenv_u64 整体替换为 env_i64(类型 u64 → i64),-1i64::MIN 等负值可正常解析;空格、下划线、非 ASCII 数字仍按「无效则 warn + 回退默认」处理。测试矩阵更新为 i64 的越界边界(±(i64::MAX+1))。
  4. 负数 batch 上限 = 不限(message/request.rsMAX_BATCH_REQS_PER_HTTP_REQLazyLock<usize> 改为 LazyLock<i64>,新增 batch_size_exceeds_limit(batch_size, limit):负数恒返回 false(禁用上限),正数比较用 u128 拓宽避免类型转换溢出;原大 batch 测试先 usize::try_from(cap) 再构造数据,并新增 negative_batch_limit_disables_the_item_cap 验证哨兵语义。
  5. duration 配置钳制bootstrap.rscleanup_sweepernative_api.rs 的 health timeout 均改为 env_i64(...).max(0) as u64,负值不再导致 Duration 构造异常。
  6. Python 侧适配(rust_server/server.pyRustServer.launch 不再拼 http_addr,仅保留 listen_port = get_serving().port + (dp_rank or 0) 用于日志展示,Server(...) 构造改为传 port_offset=dp_rank;DP 关闭时 None 保持原语义,不与单 rank 组的 rank 0 混淆。
  7. 测试配套:Rust 单元测试全部内联在各模块 #[cfg(test)] 中(共 248 个通过),无独立测试文件变更;CI 上 test_run_rust_tests.pytest_srt_endpoint.pytest_rust_native_mm_e2e.pytest_rust_native_mm_mmmu.py 经 4 轮 rerun 后全部通过。
文件 模块 状态 重要度
rust/sglang-server/src/utils/startup.rs 启动边界 added 7.95
rust/sglang-server/src/utils/environ.rs 配置解析 modified 7.53
rust/sglang-server/src/message/request.rs 请求解析 modified 6.89
rust/sglang-server/src/lib.rs 启动边界 modified 6.59
python/sglang/srt/rust_server/server.py 服务启动 modified 5.51
rust/sglang-server/src/api_server/disaggregation/bootstrap.rs 引导服务 modified 5.58
rust/sglang-server/src/api_server/native_api.rs 健康检查 modified 4.94
rust/sglang-server/src/utils.rs 工具模块 modified 3.78

关键符号

listen_addr value_error env_i64 batch_size_exceeds_limit Server::start RustServer.launch cleanup_sweeper health_routes

关键源码片段

rust/sglang-server/src/utils/startup.rs core-logic

新增的启动边界辅助模块,包含核心函数 listen_addr 与迁移来的 value_error,是本 PR 地址派生逻辑的主体。

//! startup.rs:Python 侧启动边界的辅助函数(从 lib.rs 拆出)。
//! value_error 统一启动期失败格式:" {context}: {err} "。
pub(crate) fn value_error(context: &str, err: impl std::fmt::Display) -> PyErr {
    PyValueError::new_err(format!("{context}: {err}"))
}/// listen_addr:从类型化 server_args 派生监听地址。
/// host 与基础 port 的唯一来源是 ServerArgs;DP 各 rank 只提供偏移量。
/// checked_add 显式拒绝端口溢出,避免 u16 回绕导致监听端口与预期不符。
pub(crate) fn listen_addr(
    server_args: &ServerArgs,
    port_offset: Option<u16>,
) -> Result<SocketAddr, String> {
    let offset = port_offset.unwrap_or_default();
    // 端口溢出直接报错(u16 回绕会让监听端口与预期不符)
    let port = server_args
        .port
        .checked_add(offset)
        .ok_or_else(|| format!("port {} + offset {offset} exceeds 65535", server_args.port))?;
    // bind() 返回 "host:port" 字符串;先按原样解析以保留地址族(含 IPv6),
    // 再用 set_port 覆盖端口,host 的语义不被破坏。
    let mut addr: SocketAddr = server_args
        .bind()
        .parse()
        .map_err(|err| format!("invalid host {:?}: {err}", server_args.host))?;
    addr.set_port(port);
    Ok(addr)
}
rust/sglang-server/src/utils/environ.rs core-logic

环境变量解析核心,env_u64 整体替换为 env_i64,是本 PR 与 Python EnvInt 语义对齐的关键文件。

/// env_i64:与 Python EnvInt 对齐的带符号环境变量解析。
/// 接受 i64::from_str 的完整语法(含负值);非法或越界值告警并回退默认,
/// 与 Python EnvField.get 的 warn-and-default 行为一致。
pub fn env_i64(name: &str, default: i64) -> i64 {
    read(name, default, |raw| raw.parse().ok())
}/// read:共享的"读环境变量或回退默认"逻辑。
/// 未设置 → 默认值;设置但无法解析 → tracing::warn + 默认值(绝不报错)。
fn read<T: Copy + std::fmt::Debug>(name: &str, default: T, parse: impl Fn(&str) -> Option<T>) -> T {
    let Ok(raw) = std::env::var(name) else {
        return default;
    };
    match parse(&raw) {
        Some(v) => v,
        None => {
            tracing::warn!(name, value = %raw, ?default, "invalid env value; using default");
            default
        }
    }
}
rust/sglang-server/src/message/request.rs core-logic

/generate 请求路径,batch 上限从 usize 改为 i64,引入负数哨兵语义,影响请求扇出保护逻辑。

/// 单次 /generate HTTP 请求可展开的最大调度请求数。
/// 进程静态,用 LazyLock 记忆化,避免热路径上每次 env::var 加锁;
/// 默认值 4096 由 Python 侧 environ.py 拥有。
static MAX_BATCH_REQS_PER_HTTP_REQ: LazyLock<i64> =
    LazyLock::new(|| env_i64("SGLANG_MAX_BATCH_REQS_PER_HTTP_REQ", 4096));/// 负数 limit 是"禁用上限"哨兵(与 Python 语义一致):
/// limit < 0 时恒为 false(不限制 batch 大小);
/// 正常比较用 u128 拓宽,避免 usize 与 i64 混用时的类型转换问题。
fn batch_size_exceeds_limit(batch_size: usize, limit: i64) -> bool {
    limit >= 0 && batch_size as u128 > limit as u128
}

评论区精华

辅助模块命名:server.rs 过于宽泛 style

作者在引入新文件后立即自我审查:"do not name this file server.rs. give it an another name." 理由是 server.rs 与主模块及 Server 类概念混淆,无法传达其仅服务启动边界的定位。

结论:重命名为 utils/startup.rs,import 同步更新,模块 doc 明确 startup-only 角色;rainj-me 随后批准 PR。 · 已解决

风险与影响

  1. 启动签名破坏:Server 构造器的 http_addr 参数被移除,任何仍按旧签名调用的外部脚本或嵌入方会直接 TypeError。仓库内唯一调用方 python/sglang/srt/rust_server/server.py 已同步,但独立使用者需要迁移。
  2. env 语义反转:SGLANG_MAX_BATCH_REQS_PER_HTTP_REQ 的负值从「回退默认 4096」变为「禁用上限」;若用户此前误设负值并依赖保护,现在会失去 batch 上限防护。同时大于 i64::MAX 的合法 u64 值(如 18446744073709551616)现在会回退默认。
  3. 负数 duration 钳制 0 的忙等风险:cleanup_sweeper 若被设为负值会变成 Duration::from_secs(0),tokio::time::sleep(0) 立即返回导致 sweep 忙等;比 panic 安全,但值得在文档中提示。
  4. 行为等价性:默认偏移 0 时 listen_addr 与原 http_addr 默认路径等价;DP 场景端口计算逻辑从 Python 侧移到 Rust 侧,当前 server.py 仍自行计算 listen_port 仅用于日志,若后续与 Rust 实际绑定不一致需留意。

影响范围集中在 SGLANG_RUST_SERVER=1 模式的启动路径与 DP 多 rank 端口分配。对用户而言行为基本不变,主要收益是地址配置单一事实来源、避免 Python/Rust 双端漂移,以及负数环境变量语义与 Python EnvInt 对齐。对团队而言,这是 4-PR stack 的第 2 层,为后续 #37222(schema 同步)与 #37226(请求默认值)铺路,整体方向是让 Rust server 与 Python 参考实现共享语义。

启动签名变更 env 负值语义反转 负数 duration 钳制 0 可能忙等 大数值 env 回退默认

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论