纯 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:
| crate | Rust code | 做什么 |
|---|---|---|
mistralrs-core | 314,868 | 模型、管线、调度、KV cache、前缀缓存、投机解码、工具与推理解析 |
mistralrs-quant | 99,767 | 量化格式与对应 kernel |
mistralrs-server-core | 21,113 | OpenAI 兼容的 HTTP 服务 |
mistralrs-paged-attn | 11,314 | 分页注意力的 CUDA / Metal kernel 绑定 |
mistralrs-pyo3 | 5,630 | Python 扩展 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.0tag(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 调用的 Rust | Python 自己做什么 | |
|---|---|---|
SGLang sglang-radix-tree | 前缀树的匹配、插入、驱逐 | 调度、KV 分配、前向 |
SGLang sglang-server | HTTP、分词、反分词 | 调度、前向 |
vLLM 已删的 _rust_tool_parser | 一个工具解析器 | 其余全部 |
mistral.rs mistralrs | 整个引擎 | 只组请求、收结果 |
分工对照
| 环节 | vLLM v0.32.0 默认 | SGLang v0.5.22 默认 | mistral.rs v0.9.5 |
|---|---|---|---|
| HTTP | Python(可选 Rust vllm-rs) | Python(可选 Rust sglang-server) | Rust(axum) |
| 模板与分词 | Python + HF tokenizers | Python + HF tokenizers | Rust(minijinja + HF tokenizers) |
| 调度 | Python | Python | Rust |
| KV 块 / 前缀缓存 | Python | Python 编排 + Rust 树核心(条件默认) | Rust,移植 vLLM v1 |
| 前向 | PyTorch + CUDA kernel | PyTorch + CUDA kernel | candle + 自带 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)默认关。
- 本页不做性能比较,三者没有在同一环境下测过。