Prhub

#32358 sglang rust server tokenizer manager, ring and runtime

原始 PR 作者 rainj-me 合并时间 2026-07-30 09:42 文件变更 13 提交数 3 评论 1 代码增减 +3158 / -38

执行摘要

新增 Rust TokenizerManager、Ring 和 Runtime 模块

PR body 说明 'Split PR from #29799',旨在逐步构建 Rust server 的核心运行时。通过将 tokenizer 和 detokenizer 分离到 Rust 线程,利用 flume 通道进行无锁、零拷贝的列式数据传输,可以显著降低 Python GIL 和 ZMQ 序列化带来的性能开销,提升系统吞吐并降低延迟。此 PR 是迈向 Rust server 生产化的重要一步。

该 PR 是 SGLang Rust Server 基础架构的关键拼图,设计上在内存布局(列式数据)、并发模型(flume + select)和 GIL 管理方面有很多值得学习的权衡。建议团队内所有参与核心调度和通信的开发人员仔细阅读 ring.rstokenizer_manager.rs 的设计文档和代码注释。合并后应尽快补充集成测试,并考虑在 CI 中启用 Rust server 的端到端测试。

讨论亮点

无公开审核评论(评论数为 0),核心设计决策已在之前的依赖 PR 中讨论。

实现拆解

实现拆解为以下五个步骤:

  1. 运行时配置定义(runtime/config.rs:新增 RustServerServerArgs 用于控制 Rust 特有的启动参数(HTTP 地址、线程数、ring 容量、CPU 绑定等)。RuntimeConfig 将其与 Python server_args 的派生视图 ServerArgs 组合,通过 Arc 共享确保不可变。ServerArgs 通过 serde 反序列化自动截取已知字段并忽略未知键。

  2. 消息通道层(ring.rs:定义 IngressProducer/IngressConsumerEgressProducer/EgressConsumer 两对对应 ingress(TokenizerManager→Scheduler)和 egress(Scheduler→Detokenizer)方向的通道。使用 flume crate 实现无锁、有界队列。IngressConsumer 提供非阻塞 drain 方法将多条消息累积为列式 IngressColumns(headers、ids 预分列),同时提供阻塞 wait 方法让调度器在空闲时挂起等待。EgressProducerpush 和 EgressConsumer 的 drain 类似。所有操作都是非阻塞或带超时阻塞,不长时间持有 GIL。

  3. TokenizerManager 循环(tokenizer_manager.rs:定义 TmEvent 枚举(Ingress/Tokenized)和 Senders 通道管理结构。ingress 循环驱动请求从接收到令牌化的 FSM(Received→Validating→Normalizing→Tokenizing→PreSendValidating→Queued),通过 flume::Selector 同时监听事件和关闭信号。egress 循环接收调度器的输出帧,路由到对应的 detokenizer shard。定义了 AbortSource 区分来自 Guard 和 Detok 的中止请求,确保资源安全释放。

  4. Egress 帧协议与解码(message/egress.rs:定义 EgressSink(per-request 后向通道)和 EgressItem(Frame/Done/Control/Error 变体)。新增三个帧标签(RESULT=1, BATCH=2, ERROR=3)。frame_egress_batch_cols 将列式 header 和 data columns 打包为连续字节帧(一次拷贝)。提供 take_f32/take_i32 等边界检查的解码函数,以及 BatchHeaderChunkEvent 系列结构用于列式解码。所有操作不涉及 Python 对象,无需 GIL。

  5. 采样参数验证与结束原因(message/sampling.rsfinish_reason.rssampling.rs 扩展大量硬限制(stop 字符串/正则的数量和长度),通过 normalize 方法映射 Python 语义(null→默认值),verify 方法执行校验。finish_reason.rs 定义 FinishKind(Stop/Length/Abort)和 FinishReason(Known/Unknown)枚举,支持向前兼容未知类型。AbortReason 使用 Box 缩小 ChunkEvent 体积。

此外,fsm.rs 的状态机分支也做了调整以确保新 ring 路径覆盖所有分支;utils/regex.rs 为正则编译增加了内存缓存;message.rs 适配新模块的导入。

文件 模块 状态 重要度
rust/sglang-server/src/ring.rs 通信环 added 9.08
rust/sglang-server/src/tokenizer_manager.rs 令牌化管理器 added 8.4
rust/sglang-server/src/message/egress.rs 响应消息 added 8.89
rust/sglang-server/src/message/finish_reason.rs 结束原因 added 9.02
rust/sglang-server/src/runtime/config.rs 配置模块 added 8.89
rust/sglang-server/src/message/sampling.rs 采样参数 modified 8.59
rust/sglang-server/src/utils/regex.rs 正则工具 modified 7.55
rust/sglang-server/src/fsm.rs FSM 状态机 modified 7.49
rust/sglang-server/src/message.rs 消息定义 modified 6.27
rust/sglang-server/src/runtime/runnable.rs 可运行 added 5.55
rust/sglang-server/src/runtime.rs 运行时 added 5.17
rust/sglang-server/src/message/request.rs 请求 modified 4.18

关键符号

try_send take_f32 take_i32 frame_egress_batch_cols try_push drain wait push_msg recv matched abort_status normalize verify default from_json validate_mandatory bind run cached_bound

关键源码片段

rust/sglang-server/src/tokenizer_manager.rs core-logic

定义 TokenizerManager 的主循环入口、事件类型和通道管理结构,是 Rust server 请求处理的核心编排器。

//! TokenizerManager — owns the request lifecycle across two isolated threads:
//!
//! * [`ingress`] — drives the ingress FSM (Received → Validating →
//!   Normalizing → {Tokenizing | PreSendValidating}) and pushes tokenized
//!   requests to the scheduler ring.
//! * [`egress`] — drains the scheduler-output ring and routes each chunk to
//!   the owning detokenizer shard.
//!
//! The two run on separate pinned threads with no shared state, connected to
//! the rest of the pipeline only through `flume` channels: [`TmEvent`] into
//! the ingress loop, [`Senders`] fanning out to the pools.use crate::ids::Rid;
use crate::message::{DetokMsg, Request};/// Blocking receive that also wakes on shutdown: returns `None` when `rx` closes
/// *or* the `shutdown` sender is dropped.
pub fn recv<T>(rx: &flume::Receiver<T>, shutdown: &flume::Receiver<()>) -> Option<T> {
    flume::Selector::new()
        .recv(rx, |r| r.ok())
        .recv(shutdown, |_| None)
        .wait()
}/// Events into the TokenizerManager ingress loop.
pub enum TmEvent {
    /// A freshly received request from the API server.
    Ingress(Request),
    /// A request back from the tokenizer pool.
    Tokenized(Request),
}/// Who asked for an abort.
#[derive(Clone, Debug)]
pub enum AbortSource {
    Guard(Rid),
    Detok(Rid),
}impl AbortSource {
    pub fn rid(&self) -> &Rid {
        match self {
            Self::Guard(rid) | Self::Detok(rid) => rid,
        }
    }
}#[derive(Clone)]
pub struct Senders {
    pub tm: flume::Sender<TmEvent>,
    pub abort: flume::Sender<AbortSource>,
    pub detok: Vec<flume::Sender<DetokMsg>>,
}
rust/sglang-server/src/message/egress.rs dependency-wiring

定义 egress 帧协议、编解码函数和 per-request 后向通道,是响应路径的基础设施。

//! The egress (response) direction: the per-request back-channel the API
//! handler drains ([`EgressSink`] / [`EgressItem`]), the egress-ring frame
//! encodings (batch / control result / error), and the columnar batch decode
//! into per-request [`ChunkEvent`]s.use bytes::Bytes;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use super::TokenIds;
use super::finish_reason::FinishReason;
use crate::error::Error;
use crate::ids::Rid;#[derive(Clone, Debug)]
pub enum EgressSink {
    Local(mpsc::Sender<EgressItem>),
}#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SinkError {
    Full,
    Closed,
}impl EgressSink {
    pub fn try_send(&self, item: EgressItem) -> Result<(), SinkError> {
        match self {
            EgressSink::Local(tx) => tx.try_send(item).map_err(|e| match e {
                mpsc::error::TrySendError::Full(_) => SinkError::Full,
                mpsc::error::TrySendError::Closed(_) => SinkError::Closed,
            }),
        }
    }
}#[derive(Debug)]
pub enum EgressItem {
    Frame(ChunkEvent),
    Done(ChunkEvent),
    Control(Bytes),
    Error(Error),
}pub const EGRESS_TAG_RESULT: u8 = 1;
pub const EGRESS_TAG_BATCH: u8 = 2;
pub const EGRESS_TAG_ERROR: u8 = 3;fn take_f32(data: &[u8], off: &mut usize, n: usize) -> Option<Vec<f32>> {
    let start = *off;
    let end = start.checked_add(n.checked_mul(4)?)?;
    let out = data
        .get(start..end)?
        .chunks_exact(4)
        .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
        .collect();
    *off = end;
    Some(out)
}fn take_i32(data: &[u8], off: &mut usize, n: usize) -> Option<Vec<i32>> {
    let start = *off;
    let end = start.checked_add(n.checked_mul(4)?)?;
    let out = data
        .get(start..end)?
        .chunks_exact(4)
        .map(|c| i32::from_le_bytes([c[0], c[1], c[2], c[3]]))
        .collect();
    *off = end;
    Some(out)
}pub fn frame_egress_batch_cols(header: &[u8], data_cols: &[&[u8]]) -> Bytes {
    let data_len: usize = data_cols.iter().map(|c| c.len()).sum();
    let mut buf = Vec::with_capacity(1 + 4 + header.len() + data_len);
    buf.push(EGRESS_TAG_BATCH);
    buf.extend_from_slice(&(header.len() as u32).to_le_bytes());
    buf.extend_from_slice(header);
    for col in data_cols {
        buf.extend_from_slice(col);
    }
    Bytes::from(buf)
}

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 兼容性风险:新 ring 通道替换了原有的 ZMQ 套接字,但本 PR 中 Rust server 仍通过 'embedded' 模式与 Python 调度器共存,现有 ZMQ 路径依然保持,不会影响普通用户。
  2. 死锁风险IngressConsumer::waitdrain 之间的 stash 机制以及 Mutex 的使用,如果错误地在持有 GIL 时调用可能会导致死锁。代码注释强调了调用方必须遵循约定,但缺少测试覆盖。
  3. 背压处理try_push 返回布尔值而非阻塞,调用方需要在服务级别处理 full 情况。当前实现中未看到重试或降级策略,可能导致请求丢失。
  4. 容量配置:所有 ring 容量硬编码默认值(8192),生产环境可能需要调优。未提供运行时动态调整接口。
  5. 缺少测试覆盖:Rust 代码的单元测试仅在 finish_reason.rs 中看到少量,关键路径(ring 的并发 push/drain、FSM 状态转换)缺乏测试。
  6. 内存分配IngressColumns 中的 headers 和 ids vec 可能在每次 drain 时重新分配,但考虑到 drain 后立即消费,影响有限。

该 PR 主要影响的是 Rust server 内部结构,对外用户透明。对系统内部:

  1. TokenizerManager 和调度器之间的通信不再经过 ZMQ,减少了序列化和上下文切换开销;
  2. Rust 线程可以独立执行 tokenize/detokenize,不阻塞 Python 主循环;
  3. 实验性功能,默认不启用,需通过 RustServerServerArgs 配置。对开发团队:需理解新的 ring 和 FSM 设计,后续维护需同时维护 ZMQ 和 Rust 两条路径直至完全切换。
核心路径变更 缺少测试覆盖 背压处理未闭环 跨语言接口复杂

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论