Rust in Everything

纯 Rust 引擎作对照

主线位置:整条「一个请求的一生」——HTTP、模板与分词、调度、KV 块分配、前向与采样、反分词——都在一个 Rust 进程里。本页只作对照:看 Rust 接手 GPU 前向时,引擎的其他部分长什么样。

版本:mistral.rs v0.9.5;对照 vLLM v0.32.0、SGLang v0.5.22;candle 见本站 candle 卷(0.11.0)

前几页说的是同一件事:vLLM 和 SGLang 里,Rust 只占 CPU 一侧的边(协议、文本、路由、SGLang 的前缀树),GPU 前向从来是 Python 调 CUDA kernel。mistral.rs 反过来:

  • 前向是 Rust。 张量和算子来自 candle,固定在一个 git rev 上;CUDA 和 Metal kernel 由 mistral.rs 自己补了一大批。
  • 引擎的其余部分也是 Rust。 分页注意力、调度、前缀缓存、投机解码、量化都在自己的 crate 里;KV 管理器的注释直接写着「a port of vLLM's v1 KVCacheManager」。
  • Python 在外面。 mistralrs-pyo3 把整个引擎编成 Python 扩展:Python 调 Rust,方向和 SGLang 的 PyO3 扩展相同,但被调用的是整个引擎,不是某个数据结构。

这里不比速度:本卷没有在同一台机器上跑过三者,任何数字都是待实测,而且不在 C3 的清单里。

flowchart TB
  subgraph PY["vLLM / SGLang(默认路径)"]
    p1["Python:HTTP、模板、分词"] --> p2["Python:调度、KV 块管理"]
    p2 --> p3["Python 调 CUDA kernel<br/>PyTorch、FlashInfer 等"]
    rr["Rust:可选前端、SGLang 树核心、路由器"]
  end
  subgraph MR["mistral.rs(一个进程)"]
    m1["axum HTTP<br/>mistralrs-server-core"] -->|"通道"| m2["引擎线程<br/>调度、KVCacheManager、前缀缓存"]
    m2 --> m3["模型前向<br/>candle 张量 + 自带 CUDA / Metal kernel"]
    py["Python(可选)"] -->|"PyO3"| m2
  end

工作区和规模

15 个成员 crate(Cargo.toml#L2)。tokei 统计整个仓库:Rust code 486,041 行,CUDA 34,117 行,Metal Shading Language 14,443 行。几个大的 crate:

crateRust code做什么
mistralrs-core314,868模型、管线、调度、KV cache、前缀缓存、投机解码、工具与推理解析
mistralrs-quant99,767量化格式与对应 kernel
mistralrs-server-core21,113OpenAI 兼容的 HTTP 服务
mistralrs-paged-attn11,314分页注意力的 CUDA / Metal kernel 绑定
mistralrs-pyo35,630Python 扩展 mistralrs

对比:vLLM 的整个 rust/ 是 115,234 行,SGLang 的 rust/ 里最大的 sglang-radix-tree 是 35,473 行。mistral.rs 一个 mistralrs-core 就接近前者的三倍,多出来的主要是模型实现和 kernel 封装——vLLM、SGLang 里这部分是 Python 和 CUDA。

和 candle 的关系

candle 四个 crate 都从 git 取,钉在同一个 rev(Cargo.toml#L42):

# Cargo.toml(mistral.rs)
# candle-core = { version = "0.11.0" }
# ...
candle-core = { git = "https://github.com/huggingface/candle.git", version = "0.11.0", rev = "66a8cf184a5a519671454066b1b9efd446ec9f5c" }
candle-nn = { git = "https://github.com/huggingface/candle.git", version = "0.11.0", rev = "66a8cf184a5a519671454066b1b9efd446ec9f5c" }
candle-flash-attn-v3 = { git = "https://github.com/huggingface/candle.git", version = "0.11.0", rev = "66a8cf184a5a519671454066b1b9efd446ec9f5c" }
candle-metal-kernels = { git = "https://github.com/huggingface/candle.git", version = "0.11.0", rev = "66a8cf184a5a519671454066b1b9efd446ec9f5c" }
  • 这个 rev 是 candle 2026-09-24 的提交「Fix the rotary emb convention in mimi transformer」,在 0.11.0 tag(2026-06-26)之后 62 个提交;本地 candle 克隆里没有任何 tag 包含它。所以 mistral.rs v0.9.5 用的是「0.11.0 之后的一个开发版」,和本站 candle 卷讲的 0.11.0 有差别。被注释掉的几行显示作者也在 crates.io 版本和本地路径之间切换过。
  • 用到的只有 candle-core(张量、设备、存储后端)、candle-nn(基础层和算子)、FlashAttention v3 和 Metal kernel;没有用 candle-transformers。模型自己写在 mistralrs-core/src/models/、vision_models/ 等目录下,GGUF 加载也在 mistralrs-core/src/gguf/。

candle 本身的张量、后端分派、量化和 GGUF 推理,在本站的 candle 卷里有逐层的源码走读:后端、量化、量化 Llama、设计取舍。mistral.rs 在 candle 之上补的是服务这一层:多请求并发、显存里的 KV 块怎么分、哪些请求一起算。

分页注意力:默认开,移植 vLLM v1

开关有三档,默认 auto(cli args/paged_attn.rs#L59)。auto 怎么落地看 configure_paged_attn(mistralrs_for_server_builder.rs#L1415):

// mistralrs-server-core/src/mistralrs_for_server_builder.rs
Ok(if mistralrs_core::distributed::use_nccl() {
    paged_attn.unwrap_or(defaults::PAGED_ATTN_CUDA)
} else if device.is_cpu() {
    defaults::PAGED_ATTN_CPU
} else if device.is_cuda() {
    paged_attn.unwrap_or(defaults::PAGED_ATTN_CUDA)
} else if device.is_metal() {
    paged_attn.unwrap_or(defaults::PAGED_ATTN_METAL)
} else {
    false
})

PAGED_ATTN_CUDA 和 PAGED_ATTN_METAL 是 true,PAGED_ATTN_CPU 是 false(#L135)。注意 CPU 那一支不看用户设置:CPU 上永远关,显式 on 会在前面的 ensure! 报错。

块大小默认 32 个 token(paged_attention/mod.rs#L55),只允许 8、16、32(#L527)。vLLM CacheConfig 的默认是 16,且会被注意力后端改写(见分页注意力)。

块管理照着 vLLM v1 搬(kv_cache_manager.rs#L1):

// mistralrs-core/src/paged_attention/kv_cache_manager.rs
//! KV Cache Manager for paged attention.
//!
//! This is a port of vLLM's v1 `KVCacheManager` + `FullAttentionManager`.
//! It manages block allocation, prefix cache lookups, and per-request block tracking.
//!
//! The manager owns a `BlockPool` and provides high-level operations:
//! - `get_computed_blocks`: Find the longest prefix cache hit for a request.
//! - `allocate_slots`: Allocate blocks for new tokens.
//! - `free`: Free blocks when a request completes or is preempted.
//! - `cache_blocks`: Cache newly-full blocks after computation.

模块声明里 block_hash(「Content-addressable block hashing for prefix caching」)和 block_pool(「Flat block pool with LRU free list」)都标着「vLLM v1 approach」(paged_attention/mod.rs#L5)。函数名 get_computed_blocks、allocate_slots、cache_blocks 和 vLLM Python 里的同名。机制本身见分页注意力和前缀缓存,这里不重复。

前缀缓存:两套,按是否分页选

  • 分页注意力开着:前缀缓存就是上面的块哈希,满块登记、按哈希链查最长命中,和 vLLM 一样。
  • 分页注意力关着(例如 CPU):退回 PrefixCacheManagerV2 的序列级缓存(prefix_cacher.rs#L178):以完整 token 序列为键存整份 KV cache,查找时找最长公共前缀;设备上默认最多留 16 份(PREFIX_CACHE_N,mistralrs_for_server_builder.rs#L130),多了从最旧的开始丢弃。

同一个 PrefixCacheManagerV2 在分页模式下还负责混合模型(带循环层的)的循环状态快照,按块哈希挂在对应的前缀上。

调度:预填充批和解码批交替

调度配置有两种:非分页时的 DefaultScheduler,分页时的 PagedAttentionMeta(scheduler/mod.rs#L56)。后者的默认值(#L22):

// mistralrs-core/src/scheduler/mod.rs
pub const DEFAULT_MAX_NUM_BATCHED_TOKENS: usize = 4096;
pub const DEFAULT_MAX_PREFILL_CHUNK_TOKENS: usize = 512;
pub const DEFAULT_MAX_DECODE_STEPS_BEFORE_PREFILL: usize = 8;

分页调度器(paged_attention/scheduler.rs#L921)每一步先问 completion_is_due:该不该跑解码。答案决定这一步是预填充批还是解码批,输出结构的注释写的就是「Either ALL prompt or ALL completion」(#L61)。有提示能接纳时,跑过至少一步解码就可以插入预填充;连续解码的上限是 8 步。长提示按 token 预算切块,预算是 4096,模型支持调度器可见的分块时收紧到 512。

这和 vLLM v1 不同:vLLM 每一步按一个 token 预算把预填充块和解码 token 放进同一个批(见连续批处理、分块预填充)。mistral.rs 交替的代价是解码要等预填充那一步,好处是每次前向的形状更整齐。

形状整齐还体现在「分桶」:一批里的序列要有相同的桶键(长度桶、已缓存前缀长度、媒体类型等),不同桶的序列被抢占回等待队列(#L777)。支持打包预填充(packed prefill)的模型,预填充批不按长度分桶。

flowchart LR
  w["等待队列"] --> q{"completion_is_due?"}
  q -->|"否:跑预填充"| pf["预填充批<br/>按桶键挑一组,长提示按预算切块"]
  q -->|"是:跑解码"| dc["解码批<br/>同桶的运行中序列"]
  pf --> fw["一次前向<br/>candle + kernel"]
  dc --> fw
  fw --> cnt["解码连续步数计数<br/>有提示可接纳就切预填充,上限 8 步"]
  cnt --> q

引擎循环跑在一个专门的 OS 线程上,线程里自建 tokio 运行时(lib.rs#L1232);HTTP 层通过通道把请求交给它。结构上像 vLLM 的「前端进程 + EngineCore 进程」压进了同一个进程。

投机解码:默认关

SpeculativeConfig 只有 Off 和 Mtp 两个变体(speculative/config.rs#L19):MTP 可以用目标模型自带的预测头,也可以另给一个草稿模型;另有 DFlash 块扩散草稿模型(speculative/dflash.rs)。服务端默认 MTP_CONFIG = None,即关闭。接受率和加速比没有测过;vLLM、SGLang 的投机解码在 04 部分讲。

PyO3:Python 调整个引擎

mistralrs-pyo3 编成名为 mistralrs 的 cdylib(Cargo.toml#L16),publish = false,以 Python wheel 发布。它和本部分前几页的 PyO3 方向相同(Python 调 Rust),粒度不同:

被 Python 调用的 RustPython 自己做什么
SGLang sglang-radix-tree前缀树的匹配、插入、驱逐调度、KV 分配、前向
SGLang sglang-serverHTTP、分词、反分词调度、前向
vLLM 已删的 _rust_tool_parser一个工具解析器其余全部
mistral.rs mistralrs整个引擎只组请求、收结果

分工对照

环节vLLM v0.32.0 默认SGLang v0.5.22 默认mistral.rs v0.9.5
HTTPPython(可选 Rust vllm-rs)Python(可选 Rust sglang-server)Rust(axum)
模板与分词Python + HF tokenizersPython + HF tokenizersRust(minijinja + HF tokenizers)
调度PythonPythonRust
KV 块 / 前缀缓存PythonPython 编排 + Rust 树核心(条件默认)Rust,移植 vLLM v1
前向PyTorch + CUDA kernelPyTorch + CUDA kernelcandle + 自带 CUDA / Metal kernel
多副本路由独立的 Rust 路由器(vllm-project/router)独立的 Rust 路由器(SMG)不在本页范围

两种路线的取舍可以从源码看出几点,但都不是性能结论:

  • vLLM、SGLang 把新模型、新 kernel 的成本留在 Python 生态里。 模型实现直接用 PyTorch,kernel 来自 FlashInfer 等库;Rust 只接手和模型无关的部分,所以 Rust 代码能在不同模型之间复用。
  • mistral.rs 把一切放进 Rust,模型也要自己写。 mistralrs-core 和 mistralrs-quant 两个 crate 合计约 41 万行,其中很大一部分是逐个模型、逐种量化格式的实现。换来的是单一进程、单一语言,没有 Python 解释器和跨进程序列化。
  • 机制在收敛。 mistral.rs 的 KV 管理直接移植 vLLM v1,函数名都相同;分页、块哈希前缀缓存已经是三者的共同做法,差别在调度策略和前向实现。

小结

  • mistral.rs v0.9.5 是对照组:Rust 跑前向,整个请求路径在一个 Rust 进程里,Python 只通过 PyO3 从外面调用。
  • 它基于 candle,但钉的是 0.11.0 之后 62 个提交的一个 git rev,只用 candle-core、candle-nn 和两个 kernel crate,模型自己写;candle 本身见本站 candle 卷。
  • 分页注意力在 CUDA、Metal 上默认开,CPU 上永远关;块大小默认 32;KV 管理器注明移植自 vLLM v1。
  • 分页时前缀缓存按块哈希,非分页时按整条序列、最多 16 份。
  • 调度把预填充批和解码批分开交替,连续解码上限 8 步;批内要求同一桶键。
  • 投机解码(MTP、DFlash)默认关。
  • 本页不做性能比较,三者没有在同一环境下测过。

💬 关联讨论

发起讨论
加载中...