Rust in Everything

两种前端

主线位置:一个请求的一生里「HTTP 进来 → 模板与分词」和「反分词、流式返回」这两段。默认两个引擎都用 Python 做;打开 Rust 前端后,这两段换成 Rust,调度、KV 分配和 GPU 前向仍是 Python + CUDA。
版本:vLLM v0.32.0、SGLang v0.5.22

两个引擎都有一个可选的 Rust 前端,默认都关着。做的事几乎一样:接 HTTP、渲染聊天模板、分词、把引擎吐出的 token 增量反分词再流式返回。接法相反:

  • vLLM vllm-rs:独立子进程。Python 先绑好 HTTP 和 ZMQ 的监听 socket,拉起 vllm-rs frontend,把 fd 传给它;引擎那边看到的还是原来的 ZMQ + msgpack,一行不用改。
  • SGLang sglang-server:不另起进程。它是 rank-0 调度器进程里的一组 Rust 线程,Python 调度循环直接从 Rust 的 flume 通道里取请求,生成结果整批推回去。
flowchart TB
  subgraph VL["vLLM:打开 VLLM_USE_RUST_FRONTEND 后"]
    sup["Python 主进程<br/>绑定 socket 后监护"] -->|"Popen + pass_fds"| rs["vllm-rs frontend 进程"]
    sup -->|启动| ec["EngineCore 进程<br/>调度 + GPU"]
    rs <-->|"ZMQ + msgpack"| ec
  end
  subgraph SG["SGLang:打开 SGLANG_RUST_SERVER 后"]
    mp["Python 主进程<br/>只等调度器退出"]
    subgraph r0["rank-0 调度器进程"]
      rt["Rust 线程<br/>HTTP 分词 反分词"] <-->|"flume 通道"| py["Python 调度循环 + GPU"]
    end
    mp -.->|启动| r0
  end

vLLM:vllm-rs 独立进程

怎么打开

两个环境变量:

  • VLLM_USE_RUST_FRONTEND:默认 "0"(envs.py#L1475)。
  • VLLM_RUST_FRONTEND_PATH:默认 "auto",指包目录里的 vllm/vllm-rs;找不到就抛 FileNotFoundError,不会悄悄退回 Python 前端(envs.py#L582)。

二进制随主 wheel 构建:tools/build_rust.py 把它声明成 setuptools-rust 的 Binding.Exec 目标 vllm.vllm-rs(build_rust.py#L48)。默认是可选构建,只有 VLLM_REQUIRE_RUST_FRONTEND 设成真值时缺了才报错(setup.py#L69)。

vllm-rs 有四个子命令(cli.rs#L91):frontend(被 Python 监护的子进程,本页的主角)、serve(反过来由 Rust 拉起一个 Python headless 引擎)、bench、render(不连引擎,只做模板渲染和预处理)。

Python 怎么拉起它、传哪些 fd

vllm serve 发现打开了 Rust 前端,就走多 API 服务器的启动路径 run_multi_api_server(serve.py#L151)。顺序是:

  1. 绑 HTTP 监听 socket;有 --grpc-port 时再绑一个 gRPC 的(serve.py#L298)。
  2. 用裸 socket 绑好前端一侧的 ZMQ 监听:输入一个、输出一个(serve.py#L333)。这里还不是 ZMQ socket,只是 bind + listen 过的 TCP 或 Unix socket,留给子进程「领养」(network_utils.py#L409)。
  3. 启动 EngineCore 进程,等握手完成。
  4. 构造 RustFrontendProcessManager,拼命令行,Popen(cmd, pass_fds=...)(serve.py#L354)。

命令行在 vllm/v1/utils.py(utils.py#L311):

# vllm/v1/utils.py
cmd = [
    binary_path,
    "frontend",
    "--listen-fd", str(listen_fd),
    "--input-listener-fd", str(input_fd),
    "--output-listener-fd", str(output_fd),
    "--engine-start-index", str(engine_start_index),
    "--engine-count", str(engine_count),
    "--data-parallel-size", str(data_parallel_size),
]
if grpc_sock is not None:
    # ...
    cmd.extend(["--grpc-listen-fd", str(grpc_fd)])
# ...
cmd.extend(["--args-json", args_json])
# ...
self._proc = subprocess.Popen(cmd, pass_fds=inherited_fds)

(原文每个参数一行,这里合并了。)

参数是什么Rust 一侧
--listen-fdHTTP 监听 socketaxum 直接在继承的 fd 上 accept
--input-listener-fd输入方向 ZMQ 监听(Python 以 zmq.ROUTER 的身份绑定)领养成 RouterSocket
--output-listener-fd输出方向 ZMQ 监听(以 zmq.PULL 绑定)领养成 PullSocket
--grpc-listen-fd(可选)gRPC 监听起 vllm.Inference / vllm.Control 服务
--coordinator-address(可选)数据并行协调器的统计地址(stats_update_address),是地址字符串不是 fd对接 DP 协调器
--args-json其余 CLI 参数的 JSONserde 反序列化,绕过 clap

Rust 一侧的参数定义写明了这几个 fd 的角色(cli.rs#L709)。--args-json 绕过了 clap,所以 #[arg(env = ...)] 不生效;Python 把 VLLM_ENGINE_READY_TIMEOUT_S 和 VLLM_HTTP_TIMEOUT_KEEP_ALIVE 显式塞进 JSON(utils.py#L356)。日志里打印的是去掉 api_key、hf_token 的版本。

先绑后传的好处:端口在 Python 里就定下来了,EngineCore 启动时已经知道往哪连;子进程起得慢也不会丢连接,内核的 listen 队列先替它排着。

ZMQ 套接字配对

前端一侧绑定(bind),引擎一侧连接(connect)。Rust 前端和 Python 前端用的类型完全一样,所以引擎分不出对面是谁。

方向前端(bind)引擎 EngineCore(connect)说明
请求 → 引擎ROUTERDEALER,identity = 引擎序号ROUTER 按 identity 把请求投给指定的数据并行引擎
结果 → 前端PULLPUSH所有引擎往同一个 PULL 推

出处:

// rust/src/engine-core-client/src/transport.rs
let mut input_socket = RouterSocket::new();
let input_address = input_socket
    .bind_listener(adopt_listener(input_listener_fd)?)
    .await?
    .to_string();

let mut output_socket = PullSocket::new();
let output_address = output_socket
    .bind_listener(adopt_listener(output_listener_fd)?)
    .await?
    .to_string();

Rust 用的是纯 Rust 的 zeromq crate,不是 libzmq 绑定。frontend 子命令走 connect_bootstrapped:引擎已经由 Python 握过手,Rust 只按连续的序号等每个引擎在输入 socket 上报到(transport.rs#L331)。serve 子命令不同,它自己绑一个 ROUTER 做握手(transport.rs#L182)。

sequenceDiagram
  participant P as Python 主进程
  participant E as EngineCore
  participant R as vllm-rs
  P->>P: bind HTTP 与 ZMQ 监听(裸 socket)
  P->>E: 启动并握手
  P->>R: Popen,pass_fds 传监听 fd
  R->>R: 领养 fd,成为 ROUTER 与 PULL
  E->>R: DEALER connect,按引擎序号报到
  R->>E: 请求(msgpack,经 ROUTER)
  E->>R: 输出(msgpack,经 PUSH 到 PULL)

msgpack 兼容性怎么保证

线上格式是 msgspec 编出来的 msgpack,Rust 一侧手写了对应的 serde 类型。保证两边一致的是一条字节级的夹具测试 python_msgpack_fixtures_match_rust_encoding(client.rs#L2741):

// rust/src/engine-core-client/src/tests/client.rs
#[test]
fn python_msgpack_fixtures_match_rust_encoding() {
    // ...
    let script = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/tests/python_compat.py");
    let output = Command::new(&script)
        .output()
    // ...
    let request_hex = lines.next().expect("missing request fixture line");
    // ...
}

脚本用 msgspec 编出请求、输出、logprobs、KV 连接器统计、就绪响应等十几种消息的十六进制,Rust 解码后再编码比对。要注意的一点:脚本不 import vLLM,而是在文件里复刻了一份结构体定义(例如 EngineCoreSamplingParams 写明「Mirror of real SamplingParams」,python_compat.py)。所以它证明的是「Rust 与这份复刻一致」;Python 那边改了 SamplingParams 而复刻没跟上,这条测试不会红。

覆盖了哪些端点,缺什么

Rust 路由表(routes.rs#L109):

// rust/src/server/src/routes.rs
let mut router = Router::new()
    .route("/health", get(health::health))
    .route("/metrics", get(metrics::scrape))
    .route("/load", get(load::load))
    .route("/version", get(version::version))
    .route("/kv_event_sources", get(kv_event_sources::kv_event_sources))
    .route("/v1/models", get(openai::list_models))
    .route("/v1/completions", post(openai::completions))
    .route("/v1/chat/completions", post(openai::chat_completions))
    .route("/tokenize", post(tokenize::tokenize))
    .route("/detokenize", post(tokenize::detokenize));
// ...

另外几组按开关挂:

  • /inference/v1/generate(token 进、token 出)要 --enable-scale-out,否则挂一个返回错误的占位(routes.rs#L124)。
  • LoRA 加载 / 卸载要运行时 LoRA 开关;/sleep、/pause、权重更新等控制端点只在开发模式挂(routes.rs#L137)。

Python 前端有、Rust 前端没有的端点:

端点Python 一侧出处
/v1/responsesresponses/api_router.py
/v1/embeddingsembed/api_router.py
/v1/score、/v1/rerankscoring/api_router.py
/v1/audio/transcriptionstranscription/api_router.py
/v1/messages(Anthropic 兼容)anthropic/api_router.py

还有一处行为差异:多模态的 mm_device_do_normalize 在打开 Rust 前端时被强制关掉,显式打开也只记一条 warning(model.py#L1091)。归一化于是留在 CPU 预处理里做。

Rust 前端也不做的事:路由、负载均衡、PD 协调、KV 索引。KV cache 管理器仍在 Python(vllm/v1/core/);Rust 只暴露一个 /kv_event_sources。

api_server_count 只能是 1

Python 前端在数据并行时可以起多个 API 服务器进程分摊 CPU 工作,个数默认等于 DP 大小。Rust 前端是一个多线程进程,不需要也不支持多个副本:

  • 没给 --api-server-count 时,打开 Rust 前端就默认 1,注释写的是「not applicable as it's multithreaded」(serve.py#L109)。
  • 显式给了大于 1 的值,不报错,打一条 warning 后改成 1(serve.py#L131)。
  • run_multi_api_server 里还有一道 ValueError(serve.py#L277),走 vllm serve 时前面已经改成 1,碰不到它。

--grpc-port 反过来只能和 Rust 前端一起用,和 Python 的 --grpc 互斥(cli_args.py#L470)。

被删掉的 PyO3 解析器桥

vLLM 试过另一条路:把 Rust 的工具调用解析器编成 PyO3 模块 vllm._rust_tool_parser,让 Python 前端调用。

时间提交首个包含它的发布
2026-05-20f2ace1d57 Rust 前端集成 RFCv0.22.0
2026-05-2239910f2b2 代码从独立仓库搬进 rust/v0.22.0
2026-06-1143914dd74 加 Rust 工具解析器的 Python 桥v0.24.0
2026-10-06b3082d304 删除 PyO3 工具解析器桥v0.32.0

桥存在于 v0.24.0 到 v0.31.x。在 v0.31.0 上,只有 MiniMax-M3 的解析器继承了 RustToolParser。同一天先有 5145e35e5 把它移植到 Python 的 parser engine(vllm/parser/),再由 b3082d304 删桥,去掉 1193 行(PyO3 crate 398 行、Python 适配 322 行、测试 328 行)。现在它只剩 build_rust.py 里一段注释掉的示例(build_rust.py#L58):

# tools/build_rust.py
        # Example PyO3 module (the removed tool-parser bridge):
        # RustExtension(
        #     target="vllm._rust_tool_parser",
        #     path="rust/src/parser/python/Cargo.toml",
        #     features=["pyo3/abi3-py38"],
        #     binding=Binding.PyO3,
        # ...

结果是 vLLM 的 Rust 只剩一个形态:Binding.Exec 的独立二进制,wheel 里没有任何 Rust 写的 Python 扩展模块。

crate 与规模

rust/ 工作区共 115,234 行 Rust code(含注释和空行 131,409 行)。下表是 tokei 的 code 行,括号里是总行数,都含各 crate 的 tests/ 和 benches/:

crate行数做什么
vllm-server26,345(29,743)axum HTTP、tonic gRPC
vllm-parser24,201(27,553)工具调用和推理内容解析
vllm-chat19,864(22,598)聊天模板(minijinja 加手写渲染器)、多模态预处理
vllm-bench14,698(17,004)压测客户端
vllm-engine-core-client12,032(13,732)ZMQ + msgpack 传输
vllm-text5,065(5,723)分词、增量反分词
vllm-tokenizer3,727(4,392)HF tokenizers、tiktoken 等后端
vllm-cmd3,311(3,731)CLI,产出二进制 vllm-rs
vllm-llm2,947(3,345)token 进 token 出的门面
其余 6 个合计约 3,000metrics、mock 引擎、tracing、proto 等

vllm-mock-engine(784 行)是一个假引擎:预填充立即完成、解码吐随机 token,用来单独压前端。

SGLang:sglang-server 嵌进调度器

怎么打开,谁来跑

SGLANG_RUST_SERVER 默认 False(environ.py#L1902)。打开后:

  • 主进程不再起 Python HTTP 服务器和 TokenizerManager,只等调度器退出(http_server.py#L2943)。
  • 每个调度器进程判断自己是不是宿主:pp_rank、attn_tp_rank、attn_cp_rank 都是 0 的那个(scheduler.py#L2331)。开了 DP attention 时每个 DP rank 各有一个宿主,监听 port + DP rank(lib.rs#L184)。
  • 宿主启动 Rust 服务器,并把它直接当成「从 tokenizer 收请求」的那一端(scheduler.py#L2352):
# python/sglang/srt/managers/scheduler.py
rust_server = self.get_rust_server_class().launch(self)
self.rust_server = rust_server
# The rust server *is* the ingress source: SchedulerRequestReceiver
# drains its request ring (rust_server_mode) instead of a zmq socket.
self.recv_from_tokenizer = rust_server

Python 包装类的文档字符串说得最直白:它替换掉 Python 的 api-server、TokenizerManager、DetokenizerManager,在调度器进程里以 Rust 线程运行(server.py#L1)。

flowchart LR
  cli["HTTP / gRPC 客户端"] --> ax["axum 与 tonic 线程"]
  subgraph proc["rank-0 调度器进程"]
    ax --> tm["TokenizerManager 线程<br/>模板 分词"]
    tm -->|"ToSchedulerTx<br/>flume,容量 8192"| loop["Python 调度循环<br/>recv_requests 非阻塞取"]
    loop -->|"push_decode_result_batch<br/>整批一帧"| dt["反分词分片线程"]
    dt --> ax
    loop --> gpu["前向与采样<br/>GPU"]
  end

flume 通道

边界只有两条队列。通道文件开头写明了设计(channel.rs#L1):

// rust/sglang-server/src/tokenizer_manager/channel.rs
//! In embedded mode the Rust frontend threads and the Python scheduler loop
//! share one process, so these are in-process `flume` channels — literal
//! `mpsc`/`mpmc`, no shared memory, no serialization beyond the msgpack bytes
//! the payload already is.
// ...
/// ToSchedulerTx: TokenizerManager → scheduler `recv_requests`.
/// Producers are Rust TM workers; the single consumer is the Python thread.
/// Carries [`SchedulerRequest`] (scalar msgpack header + named buffers), not a
/// single msgpack blob, so no tensor ever goes through msgpack or gets copied.

PyO3 暴露给 Python 的几个方法(lib.rs#L166):

方法做什么
Server(server_args, ..., to_scheduler_cap=8192, from_scheduler_cap=8192, ...)启动所有线程后立即返回
recv_requests(max=256)非阻塞地取一批请求;每个是 msgpack 头加命名缓冲区,缓冲区以 numpy 数组移交,不复制
wait_request(timeout_ms=1000)空闲时停在通道上,释放 GIL(py.detach,lib.rs#L226)
push_decode_result_batch(header, data_cols)一个解码步的输出整批一帧推回;满了就阻塞,形成背压
push_error(rid, msg) / push_control_result单请求失败、控制请求的结果

请求头仍用 msgpack 解码成和 ZMQ 路径相同的 TokenizedGenerateReqInput,所以调度器看到的对象不变(server.py#L241)。变的是 input_ids 这类大列不再进 msgpack,而是作为缓冲区单独传;输出方向同样把 token id 和 logprob 列按原始字节拼进一帧。

📝NOTE

通道注释说「no shared memory」,指的是通道本身。多模态特征的分发用了 POSIX 共享内存:Cargo.toml 里引入 rustix 的 shm 特性注明「for the MM feature fan-out」(Cargo.toml#L54),缓冲区可能是一个 ShmBuffer,由 Python 在 TP 广播后映射。

接管了什么

  • HTTP:/generate、/health、/health_generate(native_api.rs#L78);/v1/chat/completions(chat.rs#L49)、/v1/completions(completions.rs#L38)、/v1/models(models.rs#L16);/server_info、/model_info(info.rs#L27)。
  • 分词与反分词:原来是两个独立的 Python 进程(TokenizerManager 在主进程,DetokenizerManager 单独一个),现在是同一进程里的线程。
  • gRPC:src/api_server/grpc.rs。
  • PD bootstrap 注册表:预填充 / 解码分离时节点配对用的 /route、/register_dp_rank、/query_dp_ranks(bootstrap.rs#L353)。原来由 Python TokenizerManager 承担。
  • 多模态:只有图像,且只支持有 Rust 流水线的模型族。不支持的模型在监听之前就报错,提示取消 SGLANG_RUST_SERVER(server.py#L89)。流水线处理不了的请求直接拒绝,没有 Python 回退(lib.rs#L270)。

不能一起用的选项

选项报错位置
离线 sgl.Engineengine.py#L309:只替换 sglang serve 的 HTTP 路径
--enable-watermarkvalidation_hook.py#L104
--sidecarserving_hook.py#L386:不跑 Python sidecar 的生命周期
没有 Rust 图像流水线的多模态模型server.py#L93

依赖 Dynamo 的 crate

聊天模板、分词器、解析器不是自己写的,而是 NVIDIA Dynamo 发布在 crates.io 上的 crate(Cargo.toml#L41):

# rust/sglang-server/Cargo.toml
dynamo-tokenizers = "3.1.0"
# Keep the HTTP adapter on Dynamo's public OpenAI types, renderer, and parsers.
# `dynamo-llm` stays excluded because it pulls the full runtime just to reuse
# small service helpers.
dynamo-parsers = "=9.2.8"
dynamo-protocols = "7.0.0"
dynamo-renderer = "7.0.1"
sglang-processor = { path = "../sglang-processor", default-features = false, features = ["parser"] }

sglang-processor 自称「A thin wrapper over Dynamo's frontend crates」,规则是能直接调 Dynamo 就直接调,只在 Dynamo 和 SGLang 的 Python 行为不同或缺模型时才写 SGLang 自己的代码(README.md#L3)。对照 vLLM:它的模板和解析器是自己写的 vllm-chat / vllm-parser。

sglang-server 20,073 行 Rust code(总 22,867),比 vLLM 的 rust/ 小得多,一部分原因就在这里:文本栈外包给了 Dynamo。

两边对照

vLLM vllm-rsSGLang sglang-server
开关VLLM_USE_RUST_FRONTEND=1SGLANG_RUST_SERVER=1
形态独立进程,Binding.Exec 二进制PyO3 扩展,调度器进程里的线程
和引擎的边界ZMQ ROUTER/PULL ↔ DEALER/PUSH,msgpackflume 通道,msgpack 头 + 原始缓冲区
引擎侧改动无,引擎不知道对面是 Rust调度器的收发两端改为调 Rust 对象
模板 / 分词 / 解析自写 crateDynamo 的 crate
OpenAI 端点completions、chat、models、tokenizecompletions、chat、models,加原生 /generate
已知不支持responses、embeddings、score、rerank、audio、Anthropic messages离线 Engine、watermark、sidecar、无 Rust 流水线的多模态
副本数1 个进程(多线程)每个 DP rank 一个宿主
Rust 出错的影响面前端进程,Python 监护可见调度器进程本身

实测:只测前端开销

这一组只测 vLLM 的前端,不测引擎。对面是 vllm-mock-engine:预填充立即完成,每个输出块 1 个随机 token(词表 32000),引擎一侧几乎不花时间。所以下面的数是「前端本身能跑多快」,不是「打开 Rust 前端后服务快多少」:真实服务里大头是 GPU 前向和调度,这部分开销对两种前端相同,见 C3-2。

做法:两个前端都只起前端、不起本地引擎(--data-parallel-size-local 0),vllm-mock-engine 从握手端口接上。Python 前端是 v0.32.0 Linux wheel 的 vllm serve(VLLM_TARGET_DEVICE=cpu),Rust 前端是同一 tag 构建的 vllm-rs serve。压测客户端是 vllm-rs bench serve:random 数据集,输入 512、输出 128 个 token,分词器用 Qwen3-0.6B 的配置和分词文件(没有权重)。

一台 4 核 x86 Linux 机器上限 3 核、3 GB 的容器,两个前端轮流跑,所有轮次失败请求数都是 0:

并发请求数Python req/sRust req/sPython TTFT 均值 / P99Rust TTFT 均值 / P99
1200035.49190.5427.62 / 41.33 ms2.33 / 7.01 ms
64200057.381,180.791,101.54 / 1,656.26 ms34.35 / 70.87 ms
512512065.541,021.797,422.52 / 76,721.94 ms320.58 / 702.61 ms

按表里的 req/s 相除,Rust 对 Python:并发 1 时 190.54 / 35.49 ≈ 5.4 倍,并发 64 时 1,180.79 / 57.38 ≈ 20.6 倍,并发 512 时 1,021.79 / 65.54 ≈ 15.6 倍。Python 前端从并发 64 起请求吞吐基本不再涨(57.38、65.54 req/s),TTFT 随并发急剧变长。

M2 Max 笔记本上只跑了 Rust 前端:v0.32.0 在 PyPI 上没有 macOS 的 wheel,Python 前端装不上。

并发请求数req/s输出 tok/sTTFT 均值TTFT P99
14000578.774,0750.41 ms0.93 ms
6440005,732.8733,4256.72 ms24.28 ms
512204807,261.7928,77853.33 ms116.60 ms

两张表的机器不同,不要跨表比。

局限:

  • 容器里的 3 个 CPU 由前端、mock 引擎和压测客户端共用,三者互相抢核;宿主机上还跑着别的服务。
  • mock 引擎让引擎侧的开销为零,测的只是前端开销。换成真模型后两种前端的差距会被 GPU 前向摊薄,摊薄多少待实测(C3-2)。
  • 每个并发档位只跑了一轮。
  • ITL 没有比:Python 前端在并发 64 和 512 时把多个输出块合进一个 SSE 事件,压测客户端算出的 ITL / TPOT 显示为 0.00。

实测:只渲染(模板 + 分词)

两边都有一个不带引擎的渲染服务:Rust 是 vllm-rs render,Python 是 vllm launch render,都提供 POST /v1/chat/completions/render,返回套上聊天模板、分好词的 token id。这一段只测请求进来后、交给引擎之前的 CPU 工作。

做法:同一台 Linux 机器、同样限 3 核 3 GB 的容器,两边轮流跑。两个服务都来自 v0.32.0 的 Linux wheel(wheel 里自带 vllm-rs 二进制),模型用 Qwen3-0.6B 的配置和分词文件。Rust 自动选了工具解析器 qwen3_json、推理解析器 qwen3;Python 启动时给同样的参数(--enable-auto-tool-choice --tool-call-parser qwen3_json --reasoning-parser qwen3),不给的话,带工具的请求直接返回 400。压测客户端是 oha 1.8.0,和服务在同一个容器里;每格先预热 300 次,再发 3000 次,全部返回 200。

三种请求体:

  • 短:system 加一轮 user,187 字节,30 个 token。
  • 长:8 轮问答,11.8 KB,2,188 个 token。
  • 工具:一轮 user 加 8 个工具定义,3.0 KB,932 个 token。

两边输出的 token id 三种请求都完全一致。

请求并发Rust req/sPython req/sRust 均值 / P99Python 均值 / P99
短13,0092140.33 / 1.60 ms4.66 / 8.16 ms
短168,9842671.76 / 5.46 ms59.8 / 87.2 ms
长185068.81.17 / 2.42 ms14.5 / 22.0 ms
长162,2701247.02 / 19.8 ms128.7 / 193.8 ms
工具185992.01.16 / 2.31 ms10.9 / 16.0 ms
工具162,8061395.67 / 17.6 ms115.2 / 173.1 ms

按 req/s 相除,Rust 对 Python:短请求并发 1 / 16 约 14 / 34 倍,长请求约 12 / 18 倍,工具约 9.3 / 20 倍。单个请求(并发 1)的渲染时间,Rust 在 0.3–1.2 ms,Python 在 4.7–14.5 ms。

局限:Python 的渲染服务是单进程、一个事件循环,Rust 用满 3 个核,并发 16 的差距里有一部分来自这一点;客户端和服务共用这 3 个核;每格只跑了一轮;只测了一个模型的模板和分词文件。

待实测

  • 真模型、高并发下 Python 前端与 Rust 前端的对比,两个引擎都做。待实测(C3-2)。SGLang 的 Rust 服务器离不开调度器,只能在 GPU 机器上测。

小结

  • 两个 Rust 前端做的事相同:HTTP、模板、分词、反分词、流式返回。默认都关着。
  • vLLM 把 Rust 放在进程外:Python 绑 socket、传 fd,Rust 领养后成为 ROUTER / PULL,引擎侧零改动;兼容性靠一份复刻结构体的 msgpack 夹具测试。PyO3 桥试过,v0.32.0 前删掉了。
  • vLLM 的缺口在端点(无 responses、embeddings 等)和副本数(api_server_count 被改成 1)。
  • SGLang 把 Rust 放在进程内:rank-0 调度器直接从 flume 通道取请求,大列走缓冲区不进 msgpack;文本栈用 Dynamo 的 crate。
  • SGLang 的缺口在选项兼容(离线 Engine、watermark、sidecar)和多模态覆盖。
  • 只测前端开销(引擎是立即返回的假引擎)时,3 核容器里 vLLM 的 Rust 前端在并发 1 / 64 / 512 下的请求吞吐是 Python 前端的约 5.4 / 20.6 / 15.6 倍;只做模板加分词的渲染服务,Rust 在三种请求上快约 9–34 倍,输出的 token id 一致。真模型下的差距、以及 SGLang 两种前端的对比,待实测(C3-2)。

💬 关联讨论

发起讨论
加载中...