执行摘要
本 PR 是 sglang-server(嵌入式 Rust 前端)的第二次大规模重构:将原来集中在 message.rs、mm.rs、runtime.rs、ring.rs 等单文件的代码拆分到按语义组织的子模块,并把 Python↔Rust 边界的配置传递从 server_args_json 字符串改为 pyo3 强类型构造。合并者 merrymercy 重跑了 Rust 原生多模态端到端测试并全部通过,重构未改变对外行为,但显著改善了可维护性与配置安全性。
功能与动机
PR body 只有一句话 “Refactor the rust server by addressing comments.”,结合 commit 历史中反复出现的 “address the refactor comments” 和 “remove opaque server_args_json data type”,可以确认这次重构的直接动机是消化上一次评审意见:
- 原有代码文件过大:
message.rs、mm.rs、tokenizer_manager.rs 等承载了过多职责,评审希望按模块拆分。
Server::start 接受 server_args_json 字符串,在 Rust 侧用 serde 解析,字段缺失只能靠手写 validate_mandatory() 兜底;评审希望用强类型消除这类运行期错误。
实现拆解
-
模块化重组
- message.rs → message/{config, request, response, io_struct, ids}.rs,其中 config.rs 集中全部配置类型。
- mm.rs → multi_modality/{worker, sidecar, shm}.rs,把 worker 循环、结果侧车与 POSIX shm 传输拆成独立文件。
- runtime.rs → utils/runtime.rs,并与 threads.rs 等工具聚合。
- ring.rs → tokenizer_manager/channel.rs,命名从物理学比喻改为意图表达。
- api_server.rs → api_server/{app, common, native_api}.rs。
-
配置传递类型化
- 删除 runtime/config.rs 的 serde::Deserialize 版本,新增 message/config.rs 的 #[pyclass(frozen)] 版本,ServerArgs、ModelConfig、DisaggregationMode、MmSpec 等全部成为 pyo3 类型。
- Python 侧 RustServer._build_server_args() 用关键字构造 ServerArgs,Rust 侧 Server::start 只调用 validate() 做值校验,不再需要 from_json 和 validate_mandatory() 的双层解析。
- 效果:字段漂移、类型错误在 Python 构造时立即抛 PyValueError,而不是等到运行时返回 500。
-
语义重命名
- Ingress → Intake,ingress_ring/egress_ring → to_scheduler/from_scheduler,MmHandoff → MmEncodeResult,IngressBatch → RequestBatch。
- 文档注释同步重写,Limits 的构造从 TryFrom 降级为 From(因为 vocab_size/context_len 从必填 Option 变为直接字段)。
-
测试与 CI 配套
- 更新 test/registered/ 下的 Rust 测试路径,修复了硬编码 endpoint 源文件路径的测试用例。
- workflow 中 cargo test 从 lint 阶段移回 CPU 阶段,避免早期失败。
- 最后修正 est_time 值,收尾合并。
rust/sglang-server/src/message/config.rs
本次重构的核心:将 server_args 从 JSON blob 解析改为 pyo3 pyclass 强类型构造,并汇聚 RustServerServerArgs、RuntimeConfig、ModelConfig、MmSpec 等配置类型,是 Python↔Rust 边界的单一 schema。
//! Python 侧通过 `_server.ServerArgs(...)` 按关键字构造;
//! 每个字段都是必填且类型化,pyo3 在构造时即校验,漏传或错型直接抛异常。
#[pyo3::pymethods]
impl ServerArgs {
#[new]
#[pyo3(signature = (*,
model_path, served_model_name, tokenizer_path, revision, load_format,
weight_version, host, port, log_level, log_level_http, chat_template,
tool_call_parser, reasoning_parser, stream_response_default_include_usage,
tokenizer_worker_num, detokenizer_worker_num, skip_tokenizer_init,
incremental_streaming_output, disaggregation_mode, model_config,
preferred_sampling_params, allow_auto_truncate, enable_return_hidden_states,
num_reserved_tokens, version, max_total_num_tokens,
))]
#[allow(clippy::too_many_arguments)]
fn py_new(
model_path: String,
served_model_name: String,
tokenizer_path: String,
revision: Option<String>,
load_format: Option<String>,
weight_version: Option<String>,
host: String,
port: u16,
log_level: String,
log_level_http: Option<String>,
chat_template: Option<String>,
tool_call_parser: Option<String>,
reasoning_parser: Option<String>,
stream_response_default_include_usage: bool,
tokenizer_worker_num: usize,
detokenizer_worker_num: usize,
skip_tokenizer_init: bool,
incremental_streaming_output: bool,
disaggregation_mode: DisaggregationMode,
model_config: ModelConfig,
preferred_sampling_params: Option<PreferredSamplingParams>,
allow_auto_truncate: bool,
enable_return_hidden_states: bool,
num_reserved_tokens: u64,
version: String,
max_total_num_tokens: u64,
) -> Self {
Self {
model_path, served_model_name, tokenizer_path, revision, load_format,
weight_version, host, port, log_level, log_level_http, chat_template,
tool_call_parser, reasoning_parser, stream_response_default_include_usage,
tokenizer_worker_num, detokenizer_worker_num, skip_tokenizer_init,
incremental_streaming_output, disaggregation_mode, model_config,
preferred_sampling_params, allow_auto_truncate, enable_return_hidden_states,
num_reserved_tokens, version, max_total_num_tokens,
}
}
}
rust/sglang-server/src/tokenizer_manager/to_scheduler.rs
原 ingress.rs 重命名并改造为 Intake 状态机,是请求进入调度器的核心路径;同时 Limits 从 TryFrom 改为 From,channel 语义全面切换为 to_scheduler/from_scheduler。
/// Intake 是单消费者阶段:只消费一个 inbox(由 API server 与 tokenizer 池共同喂入),
/// 驱动请求状态机后把请求 *move* 到下一阶段;全程无共享,不需要锁。
impl Runnable for Intake {
fn run(mut self) {
loop {
// 用 Select 而不是 drain-then-block:abort 在 inbox 空闲时到达也必须立即处理。
let next = flume::Selector::new()
.recv(&self.abort_rx, |r| r.ok().map(Lane::Abort))
.recv(&self.tok_manager_rx, |r| r.ok().map(Lane::Event))
.recv(&self.shutdown, |_| None)
.wait();
match next {
Some(Lane::Abort(rid)) => self.on_abort(rid),
Some(Lane::Event(TmEvent::Intake(req) | TmEvent::Tokenized(req))) => {
self.drive(req) // 新请求与 tokenizer 池返回的请求走同一条驱动路径
}
Some(Lane::Event(TmEvent::MmEncoded { rid, input_ids })) => {
self.on_mm_encoded(rid, input_ids)
}
Some(Lane::Event(TmEvent::MmFailed { rid, message })) => {
self.on_mm_failed(rid, message)
}
None => {
// 收到 shutdown,或 inbox 已关闭:先把 abort 队列中仍在途的请求处理完。
// selector 可能在看到 pending abort 之前就报告 inbox 关闭。
while let Ok(source) = self.abort_rx.try_recv() {
self.on_abort(source);
}
return;
}
}
}
}
}
评论区精华
- 维护者 merrymercy 对 CI 不放心,指定重跑:
test/registered/core/test_srt_endpoint.py、test/registered/vlm/test_rust_native_mm_e2e.py、test/registered/vlm/test_rust_native_mm_mmmu.py、test/registered/rust/test_run_rust_tests.py。
- github-actions 反馈 “Results for /rerun-test …” 全部 ✅;同时提示
test_cargo_workspace.py 已不存在,验证了测试文件随重构迁移。
- 无实质 review 线程,说明重构方案在作者和 merge oncall 之间已达成一致。
风险与影响
- 核心路径变更:
to_scheduler.rs / from_scheduler.rs 是请求与调度器之间的唯一通道,重命名和 channel API 调整影响所有下游调用。
- 大范围重命名:仍有硬编码路径的测试(如 endpoint 源文件路径)被修复,但可能还有遗漏。
- 边界行为变化:
server_args 从宽松 JSON 变为必填字段,Python 侧若漏传将直接 boot 失败,这符合预期,但迁移期需要同步更新所有构造点。
- 测试覆盖有限:CI 仅覆盖 endpoint 与 Rust 原生多模态路径,未覆盖全部 family(如扩散、PD 场景)。
关联脉络
本 PR 与 Python 侧的 config 重构系列(#35905、#35906、#35907 等)方向一致:都在消除 “字符串/字典 + 运行期解析” 的配置传递方式,转向单一 schema 与编译期/构造期校验。它们共同支撑后续 Rust Server 更多模块接管 Python 逻辑的演进。
参与讨论