2026 年 7 月,一个开源推理框架的 GitHub issue 区吵得很热。
起因是一位 C++ 老手抛出的问题:scheduler 换成 Rust 后,p99 延迟降了 18%,吞吐却掉了 12%,这笔账到底怎么算?
回复里很快堆满了 perf 火焰图、ASan 报告和几句火气不小的评论。有人说 Rust 把内存问题提前挡住了,也有人反问:如果 unsafe 写得到处都是,还不如继续用 C++。
放在推理引擎里,这不是一道语言选择题。Rust 过去更多出现在 tokenizer、工具链、外围服务里,现在开始碰 scheduler、KV cache 管理这类更靠近核心路径的组件。问题也变得具体:内存安全值多少钱,FFI 边界会吃掉多少性能,哪些模块值得迁,哪些模块最好别急着动。
这次重写改的是调度器
被讨论最多的,是一个团队把 vLLM 风格的 continuous batching 调度器从 C++14 迁到 Rust 1.85。
调度器本身不计算 attention。它做的是另一件很要命的事:请求不断进来时,决定哪些序列进入同一个 batch,再交给 GPU kernel。它不直接决定单次矩阵计算有多快,却会影响尾延迟、吞吐和显存利用。
迁移前后的代码规模大致如下:
- C++ 版本:核心调度逻辑约 4800 行,另有 1200 行内存池管理
- Rust 版本:核心逻辑约 3900 行,单独的内存管理代码基本被所有权模型吸收
压测环境保持一致:8×A100 80GB,模型是 Llama-3.1-70B 的 FP8 量化版,请求模式模拟 Chatbot 场景,平均输入 512 token,输出 128 token,泊松到达。
结果并不适合拿来做宣传海报:
- p99 延迟:C++ 版本 287ms,Rust 版本 236ms,下降 17.8%
- 吞吐量:C++ 版本 1243 token/s,Rust 版本 1094 token/s,下降 12.0%
- 连续运行 6 小时后的内存碎片率:C++ 版本约 14.3%,Rust 版本约 3.1%
- 72 小时压测中的 Segfault:C++ 版本 7 次,Rust 版本 0 次
延迟更好,吞吐更差,内存更稳,崩溃消失。C++ 老手追问的重点其实是:为什么这些指标会一起出现?
吞吐损失卡在 FFI 上
火焰图拉出来后,吞吐下降的主要原因很清楚:FFI 边界上的序列化开销。
Rust 调度器完成 batch 决策后,还要把结果交给 C++ 写的 GPU kernel launcher。原来的 C++ 版本没有这层边界,调度器和 launcher 用同一套数据结构,传指针就能继续往下走。
换成 Rust 后,batch 信息需要整理成 C ABI 能接受的内存布局。token ID 序列、attention mask、位置编码表,都要在边界处打包。压测中每秒平均发生 37 次调度决策,每次打包开销约 0.8ms。
0.8ms × 37,接近 29.6ms/s。吞吐差值里,相当一部分就耗在这里。
鲲鹏昇腾开发者社区 CodeWhim 在 2025 年 10 月的技术分析中提到,Rust 与 C/C++ 的互操作已经成熟,但 FFI 边界上的数据序列化和反序列化,在高频调用场景里仍会成为瓶颈。放到推理引擎热路径上,这个问题会被放大。
所以 issue 里那句难听话并非完全没道理:用 Rust 写调度器,再 FFI 调 C++ launcher,相当于在最热的路径上加了一层序列化税。
p99 下降,不是因为语言自带加速
p99 延迟下降也不是玄学。
C++ 版本的调度器里,有一个细粒度锁的 KV cache 管理器。多个请求线程同时读写 cache 块,需要争抢锁。负载上来后,尤其是 QPS 超过 120 时,尾延迟会被锁竞争拉高。
Rust 重写后,很多共享可变状态被所有权系统提前拦住。不该共享的,编译期就过不去;必须共享的,才会被放进 Arc<Mutex<>> 这类边界更明确的结构里。
压测报告里有一句注释很值得看:Rust 版本中,KV cache 块的分配和回收完全由所有权生命周期管理,删掉了原来 274 行 lock/unlock 逻辑。
这 274 行以前靠工程师经验维持并发秩序。迁到 Rust 后,一部分约束交给了编译器。
当然,代价也在。团队里两个 C++ 老手花了两周和 borrow checker 磨合。复盘里有人写得很直:刚开始觉得是在跟编译器打架,后来才发现,编译器是在帮他处理几年前留下的坑。
内存碎片比纸面吞吐更烦人
内存碎片率从 14.3% 到 3.1%,看起来没有 p99 那么醒目,但线上运维会很在意。
在 8×80GB 的显存池里,碎片率高意味着有不少空间“看起来可用,实际分不出去”。大 batch 请求可能因为找不到连续显存块被挡住;OOM 也未必是真用满,而是碎片化造成的假性 OOM。这个团队的 C++ 版本曾经需要每 8 小时重启一次推理服务,用来缓解碎片问题。
Rust 版本连续运行 6 小时后,碎片率压到约 3.1%。原因不是 Rust 会自动让程序变快,而是分配和释放时机更清楚。RAII 让 KV cache 块在生命周期结束时归还,所有权模型也减少了忘记释放、重复释放这类错误。
对线上服务来说,少一次半夜重启,有时比账面上多出来的 token/s 更值钱。
先动 CPU 侧,别急着碰 kernel
这个案例给出的迁移边界很清楚:Rust 更适合接手 CPU 侧、并发重、内存状态复杂的组件。
tokenizer 是一个典型起点。它是纯 CPU 逻辑,字符串处理密集。C++ 里常见的 buffer overflow 风险,在 Rust 的字符串切片和所有权规则下会少一些。
scheduler 也适合试。它频繁做并发决策,状态变化快,锁逻辑复杂。Rust 不会让调度算法自动变聪明,但能把不少数据竞争拦在编译期。
detokenization、response formatting 这类后处理模块也值得考虑。它们逻辑琐碎、安全敏感,平时容易被低估,出了问题又会直接影响线上结果。
不太适合一上来就动的,是 GPU kernel launcher 和 attention 算子。
GPU kernel launcher 本质上是 CUDA 或 CANN API 的薄封装,和 driver 强耦合。强行改成 Rust,收益未必大,维护成本可能很快上来。
Attention 算子更典型。手写 CUDA kernel,或者用 FlashAttention-3 这类 C++ 模板,很多性能来自对硬件和编译器行为的细抠。Rust 的泛型体系在这里不一定能帮上忙;unsafe 一多,内存安全优势也会被削弱。
百度智能云《Rust驱动AI:构建高性能深度学习模型推理框架的实践指南》也提到,Rust 的收益更容易出现在内存管理密集的组件中;GPU kernel 这类底层计算模块,C++ 仍然是务实选择。
边界切错,再好的语言也会亏
那个 issue 最后没有用一句“Rust 赢了”或“C++ 赢了”收场。
项目维护者做了一个更工程化的调整:调度器保留 Rust 版本,但 FFI 接口不再每次传完整 batch 数据,改成传 batch ID 和共享内存句柄;GPU launcher 继续用 C++;tokenizer 和后处理再逐步迁到 Rust。
调整后,序列化开销从 0.8ms/次降到 0.12ms/次,吞吐回升约 9%,基本追平 C++ 版本,同时保留了延迟和内存安全上的收益。
这才是推理引擎里更常见的答案:整条 pipeline 不必重写,C++ 也不会被一脚踢出去。关键是把边界切对。
如果团队正在考虑用 Rust 改推理引擎,第一步不该是立项重写。先拉 perf 火焰图,看 CPU 侧哪段最热,哪段锁最多,哪段内存状态最乱。tokenizer、scheduler、postprocessor 可以先试;GPU kernel 和算子实现,通常继续让 C++ 扛着更稳。
Rust 在推理引擎里的位置,现阶段更像是接管那些 C++ 写起来危险、调起来痛苦、崩了会惊醒人的组件。
那位 C++ 老手最后留了一句话,被点了 800 多个赞:
“我写了 15 年 C++,最近三个月用 Rust 重写了两个组件。不是因为我喜欢 Rust,是因为我老了,不想半夜三点被 Segfault 叫起来修内存越界了。”