执行摘要
- 一句话:源码检出按需构建 Rust 扩展,统一三 crate 模块命名
- 推荐动作:值得精读,尤其 loader.py 的指纹缓存、文件锁与原子暂存设计,对构建系统开发者有直接借鉴价值。建议关注两个后续优化点:一是缓存失效以整个 rust 工作区为粒度,可优化为按 crate 依赖子图计算 digest;二是 auto 模式下无 Rust 工具链时直接抛错,可考虑先探测 cargo 可用性并给出更友好的降级提示。
功能与动机
PR body 明确指出:Source checkouts (editable installs) currently require the PyO3 extensions (sglang.srt.grpc._core, sglang.srt.server._core, sglang.srt.multimodal._core) to be prebuilt into the wheel: importing them from a plain checkout fails with ModuleNotFoundError (or, for --grpc-port, a hard RuntimeError)。目标是让带有 Rust 工具链的源码检出开箱即用,消除纯 checkout 开发时必须先构建 wheel 的额外步骤,降低 SGLang 开发者调试 gRPC、RustServer 与 inkling 多模态路径的门槛。
实现拆解
按以下 5 步推进实现:
-
新增按需加载器:新建 python/sglang/srt/rust_extensions/loader.py(412 行),核心函数 load_rust_extension 实现三级加载;crate 通过扫描 rust/ 工作区所有 Cargo.toml 的 [package.metadata.sglang] python-module 元数据定位(与 setup.py 同源),新增 crate 无需任何注册代码;Python 3.10 兼容通过 tomllib/tomli 回退实现。
-
统一扩展模块命名与 wheel 布局:三个 Rust crate 的 #[pymodule] 入口从 _core 分别改为 _multimodal(sglang-mm)、_grpc(sglang-grpc)、_server(sglang-server),Python 侧模块路径统一收敛到 sglang.srt.rust_extensions.*;新建 rust_extensions/init.py 导出入口;python/setup.py 同步 wheel 内模块名;python/sglang/srt/server/init.py 调整使扩展包在源码检出可导入;同时删除 python/sglang/srt/grpc/init.py 中的旧模块残留。
-
三处调用点接线:http_server.py 的 _start_native_grpc_server_for_runtime、rust_server.py 的 RustServer.launch、image_processing_rust.py 的 inkling 导入全部改走 load_rust_extension;其中 gRPC 启动路径删除了原先“扩展不在 wheel 中则硬报错”的 try/except 分支。
-
缓存、并发与三态控制:_build_context 以源码内容 hash(_source_digest)、工具链版本、Python ABI(EXT_SUFFIX/SOABI 等)、构建环境变量(RUSTFLAGS 等)生成 fingerprint 与 target_fingerprint;_filesystem_lock 用 fcntl 文件锁串行化多进程并发构建;构建完成后复核源码 digest 防中途变化;_stage_atomically 原子暂存产物;environ.py 新增 SGLANG_RUST_BUILD_MODE=auto|never|force 环境变量契约。
-
配套依赖与测试:rust/Cargo.lock 通过 cargo update --precise 定向升级 dynamo 家族(dynamo-protocols 5.1.0→5.3.1 等),并在 rust/sglang-server/src/api_server/openai/chat.rs 的 chat_logprobs 中填充新 token_id 字段;scripts/ci/utils/stage_rust_ext_modules.sh 更新 CI 暂存路径;新增 test/registered/rust/test_rust_extension.py(350 行,纯 mock 不触发真实 cargo),另有多处测试/导入路径跟随调整(test_utils.py、_mm_rust_utils.py、test_server_args.py、bench_parity.py)。
关键文件:
python/sglang/srt/rust_extensions/loader.py(模块 扩展加载;类别 source;类型 dependency-wiring;符号 _CrateSpec, _BuildContext, load_rust_extension, _import_bundled_extension): 本 PR 核心新增文件,412 行实现三级按需加载、crate 发现、指纹缓存、并发锁与原子暂存,是所有 Rust 扩展导入的统一入口。
test/registered/rust/test_rust_extension.py(模块 单元测试;类别 test;类型 test-coverage;符号 _hold_filesystem_lock, _FailingExtensionLoader, TestRustExtension, test_bundled_wheel_extension_never_touches_source_or_cargo): 350 行 mock 测试,覆盖加载器全部决策路径,是理解指纹与缓存设计意图的最佳入口,也是后续维护的回归护栏。
python/sglang/srt/entrypoints/http_server.py(模块 启动入口;类别 source;类型 dependency-wiring): gRPC 启动路径从硬报错改为按需加载,是用户可见行为变化最明显的接线点。
python/sglang/srt/managers/rust_server.py(模块 Rust 服务;类别 source;类型 dependency-wiring): RustServer.launch 是 Rust 原生 server 的入口,Type 引用与运行导入同步迁移到新命名空间。
python/sglang/srt/multimodal/inkling/image_processing_rust.py(模块 多模态预处理;类别 source;类型 dependency-wiring): inkling 多模态预处理是第三个接线点,验证 sglang-mm 扩展在 Python 侧的消费方式。
rust/sglang-server/src/lib.rs(模块 服务内核;类别 source;类型 core-logic;符号 _core, _server): Rust server crate 的 PyO3 入口更名,是命名空间统一的关键一端。
rust/sglang-grpc/src/lib.rs(模块 gRPC 内核;类别 source;类型 core-logic;符号 _core, _grpc): gRPC crate 的 PyO3 入口更名,与 http_server 接线对应。
rust/sglang-mm/src/lib.rs(模块 多模态内核;类别 source;类型 core-logic;符号 _core, _multimodal): 多模态 crate 的 PyO3 入口更名,并同步更新 crate 文档注释。
python/setup.py(模块 构建配置;类别 source;类型 core-logic): wheel 构建配置必须与加载器发现逻辑保持一致,否则打包出的模块路径与运行期期望不符。
scripts/ci/utils/stage_rust_ext_modules.sh(模块 CI 脚本;类别 infra;类型 infrastructure): CI 暂存脚本路径同步更新,否则打包产物在 CI 环境中的布局与本地不一致。
python/sglang/srt/environ.py(模块 环境配置;类别 source;类型 core-logic): 新环境变量 SGLANG_RUST_BUILD_MODE 的运行契约在这里登记。
rust/sglang-server/src/api_server/openai/chat.rs(模块 对话接口;类别 source;类型 entrypoint): 伴随 Cargo.lock 升级,chat_logprobs 填充新增 token_id 字段,是依赖升级在业务逻辑上的落地。
rust/Cargo.lock(模块 依赖锁;类别 other;类型 core-logic): 定向升级 dynamo 家族,是 chat_logprobs token_id 填充与 --locked 可复现构建的基础。
关键符号:load_rust_extension, _discover_crate, _build_context, _source_digest, _source_files, _import_bundled_extension, _filesystem_lock, chat_logprobs
关键源码片段
test/registered/rust/test_rust_extension.py
350 行 mock 测试,覆盖加载器全部决策路径,是理解指纹与缓存设计意图的最佳入口,也是后续维护的回归护栏。
def test_fingerprint_is_content_based_and_covers_build_inputs(self):
with TemporaryDirectory() as directory:
workspace = self._workspace(Path(directory))
crate = rust_extension._discover_crate(workspace, "demo._core")
with mock.patch.object(
rust_extension,
"_command_version",
side_effect=lambda command, *args, **kwargs: f"{command} 1.0",
):
first = rust_extension._build_context(crate)
source = workspace / "demo" / "lib.rs"
# 只改动 mtime 不得改变指纹,证明指纹基于内容 hash 而非时间戳,
# 避免 git checkout 等操作触发无谓的全量重建。
os.utime(source, (1, 1))
self.assertEqual(first, rust_extension._build_context(crate))
# 内容变化必须让 fingerprint 变化,从而命中新的缓存路径。
source.write_text("fn changed() {}\n", encoding="utf-8")
changed_source = rust_extension._build_context(crate)
self.assertNotEqual(first.fingerprint, changed_source.fingerprint)
# RUSTFLAGS 属于构建环境变量,改变它应同时影响
# fingerprint 与 target_fingerprint(后者用于隔离编译产物)。
with mock.patch.dict(os.environ, {"RUSTFLAGS": "-Ctarget-cpu=native"}):
changed_flags = rust_extension._build_context(crate)
self.assertNotEqual(changed_source.fingerprint, changed_flags.fingerprint)
self.assertNotEqual(
changed_source.target_fingerprint,
changed_flags.target_fingerprint,
)
def test_auto_builds_once_then_uses_cache(self):
# auto 模式首次触发 cargo build,之后同一 fingerprint 直接命中缓存,
# 验证 cargo_build 只被调用一次,避免重复编译。
with TemporaryDirectory() as directory:
root = Path(directory)
workspace = self._workspace(root)
artifact = root / "libdemo_extension.so"
artifact.write_bytes(b"extension")
context = rust_extension._BuildContext("source", "fingerprint", "target")
loaded = ModuleType("demo._core")
with (
mock.patch.object(
rust_extension, "_import_bundled_extension", return_value=None
),
mock.patch.object(rust_extension, "_build_context", return_value=context),
mock.patch.object(rust_extension, "_source_digest", return_value="source"),
mock.patch.object(
rust_extension, "_cargo_build", return_value=artifact
) as cargo_build,
mock.patch.object(
rust_extension, "_load_extension_from_path", return_value=loaded
),
):
self.assertIs(
rust_extension.load_rust_extension(
"demo._core", workspace=workspace, cache_dir=root / "cache"
),
loaded,
)
self.assertIs(
rust_extension.load_rust_extension(
"demo._core", workspace=workspace, cache_dir=root / "cache"
),
loaded,
)
cargo_build.assert_called_once()
评论区精华
本 PR 无外部 review 讨论(review 评论为 0),唯一评论是作者 merrymercy 触发的 CI 重跑指令 "/tag-and-rerun-ci"。设计决策主要沉淀在 PR body 与 commit 演进中:
commit 2 "Fix Python 3.10 TOML compatibility":确认 loader 需要兼容 3.10,引入 tomli 回退,避免新加载器自身成为版本门槛。
commit 3 "Group Rust extensions under explicit package":扩展从散落在 sglang.srt.grpc/server/multimodal 下收敛为独立 rust_extensions 包,说明作者在迭代中意识到统一命名空间对发现与缓存的重要性。
commit 4/5/6 依次跟进 CI 暂存路径与各导入点验证,体现“命名迁移必须同步修改所有引用方”的工程约束。
风险与影响
关联脉络
- PR #34309 标题未在材料中提供(PR body 提及移除 test_cargo_workspace.py): 本 PR 原计划将 test/registered/rust/test_cargo_workspace.py 的 cargo test --workspace 改为 --locked,但该文件已被 #34309 移除,故该部分变更被丢弃而非重新添加。
参与讨论