过去一两年,AI基建这个圈子里最大的选型变化,我觉得不是又出了多少新模型,而是底层工程语言悄然切换到Rust。这里说的AI基建,是指大模型从训练完成到上线服务中间那一整条链路:向量检索、特征存储、推理引擎、模型加载、批处理调度、资源管理等。以前这些组件多数由C++或者JVM系技术栈承担,核心逻辑大家也都默认用C++来写。但现在越来越多团队在做新项目时,第一优先级直接就是Rust。
这个转变不是一家两家的偶然选择。我观察到的信号主要集中在三个层面:向量检索与特征存储组件开始批量更换内核;推理引擎和模型服务层逐渐从C++阵地迁移;AI调度器、数据管线这类全新项目从第一天就直接选Rust。这不是“某某语言更好”的口舌之争,而是工程指标驱动的务实决策。下面把这几个信号拆开讲,也会把我在实际接入Rust过程中踩过的坑和验证过的做法一并整理出来。
1. 信号一:向量检索与特征存储组件开始批量更换内核
1.1 检索层为什么成为“最痛”的一环
在线推荐、问答系统、多模态搜索这类场景,几乎都离不开向量检索。向量检索组件对延迟极敏感,对多线程并发读的要求极高,而且内部要维护复杂的索引结构,比如层级可导航小世界图、倒排索引、哈希索引等。这些数据结构涉及大量内存指针的移动、节点的插入删除和版本更新,用C++写,一不留神就是野指针或者悬垂引用;用Java或Python写,GC暂停和解释器开销又会直接吃掉延迟预算。
我接触过一个在线检索团队,原来的内核是C++写的近邻索引,服务框架在另一门高级语言上。线上时不时会出现段错误,每次都要从几十GB的核心转储文件里找线索,找到最后往往发现是某个并发路径下的生命周期管理出了问题。这类问题在C++里极其隐蔽:可能只在特定索引合并时机、特定并发量下复现,修复一个又会引出下一个。本质上,检索层对内存生命周期的要求,和C++手动管理内存的脆弱性形成了尖锐冲突。
Rust在这个环节的优势正好打在痛点上。所有权和借用规则把“谁拥有这份索引数据、谁有权修改、什么时候释放”全部放到编译期检查。索引重建、节点淘汰、并发读这些原本容易出事的操作,在Rust里写出来后,只要编译通过,内存层面的事故就基本被拦在门外。而且Rust无GC,延迟曲线稳定,不会出现分代回收导致的周期性能抖动。
1.2 换Rust内核后,我看到的技术指标变化
那个团队后来把索引内核用Rust重写了,外层接口保持不变。上线后的数据让我印象很深:同等压力下,单机QPS比原来提升了约20%到30%,P99延迟下降了明显的一截。但更值钱的不是这两个数字,而是稳定性。重写之后,连续几个月的线上运行都没有再出现内存崩溃类故障。
这不是个例。向量索引、特征存储这类组件有一个共性:读多写少、内存结构复杂、单实例需要支撑大量并发。Rust的安全并发模型在这些场景下,比“C++性能好但风险高”和“Java并发成熟但内存占用大”都更均衡。很多团队并不是因为热爱Rust才换,而是排查内存问题的成本已经高到无法忍受。
有一点必须说清楚:用Rust重写不一定意味着性能大幅提升。如果原C++实现已经优化得足够彻底,Rust很难在纯计算上拉开质的差距。真正的收益来自两点:一是内存安全带来的稳定性红利,省去了大量排障时间;二是所有权模型倒逼设计更清晰,让并发路径上的隐性耦合提前暴露。如果你期望“换成Rust就一定快一倍”,这个预期通常不成立;如果你期望“换成Rust后半夜不再被报警电话叫醒”,这个大概率能兑现。
1.3 检索组件换Rust时最容易忽略的工程细节
- 外部接口要保持兼容,先加一层FFI边界,把Rust内核包成旧接口,避免一次伤筋动骨。
- 索引结构对内存布局极度敏感,建议先用性能剖析工具确认热路径,再动手优化,不要上来就写一堆不安全代码。
- 需要支持动态增删索引时,优先用“版本化引用”而不是裸指针,这样能把Rust的安全边界维持住。
- 发布前一定要做长时间浸泡测试,Rust下内存问题少,不代表并发逻辑不会出别的错。
2. 信号二:推理引擎和模型服务层开始从C++阵地迁移
2.1 推理引擎要同时管住内存、并发与延迟
模型训练阶段可以容忍慢和偶尔不稳定,但推理服务不行。在线推理对单次请求延迟、吞吐、显存和内存占用都有硬约束。传统推理引擎选择C++,核心原因是性能和底层算子生态绑定很深。但C++带来的老问题在推理场景里同样存在:模型权重加载、KV Cache管理、动态批处理调度,这些模块都涉及共享状态和复杂生命周期,稍不注意就是内存越界或者死锁。
推理服务还有一个特点:并发模型特别复杂。一个请求进来,要经过分词、张量形状推断、算子执行、采样、结果后处理;多个请求之间又要做动态批处理,把不同长度的序列拼到同一个Batch里执行。这种高并发、高动态的场景,用Java等高并发语言可以写,但内存占用通常偏高;用C++可以写得极致,但对开发者的要求非常高。Rust的异步并发模型和处理有界内存管理的思路,在这里显得特别顺手。
KV Cache就是一个很直观的例子。不同请求会占用不同长度的缓存块,请求结束要立刻释放,批处理又要把这些缓存块动态组装。在C++里,缓存池管理通常要手动处理所有分配归还逻辑;在Rust里,所有权机制让“谁借了缓存块、谁该归还”这种逻辑直接在类型系统里表达。我见过不止一个团队吐槽KV Cache管理是推理引擎里最容易出内存碎片和泄漏的地方,而用Rust后这部分变得相当可控。
2.2 Rust与C++、Go、Java的选型对比
很多人在选型时纠结的并不是Rust本身好不好,而是它相对其他语言到底强在哪。我用一张表来总结自己在AI基建场景下的权衡:
| 维度 | C++ | Go | Java/Scala | Rust |
|---|---|---|---|---|
| 性能上限 | 最高 | 高 | 中 | 接近C++ |
| 内存安全 | 差,需人工保证 | 好,有GC | 好,有GC | 好,编译期保证 |
| 延迟稳定性 | 好 | 一般 | 一般,GC有抖动 | 好,无GC |
| 并发模型表达力 | 弱,靠经验 | 简单但较粗 | 线程池为主 | 所有权+异步,精细 |
| 部署产物 | 依赖动态库较多 | 单二进制 | 需要JRE,偏重 | 单二进制 |
| 与C/C++生态互操作 | 天然 | 一般 | 一般 | 良好 |
| 工程学习成本 | 中高 | 低 | 中 | 高 |
从这张表能看出来,如果只是写一个普通后端CRUD服务,选Go或Java更划算。但推理引擎这类组件要求延迟稳定、内存可控、并发精细、还要能垫底接住C/C++生态,Rust就成了一个在各项指标上没有明显短板的选项。Go虽然开发效率高,但其内存模型和调度模型在极致延迟场景下不如Rust可控;Java性能足够,可GC抖动的不可预测性让它在在线推理的高分位延迟上很吃亏。
2.3 落地到推理服务的几个具体改进点
第一,模型加载与预热。Rust做模型权重解析、格式转换时,可以写出无GC干扰的加载路径。配合内存映射,能有效减少大模型加载时的物理内存峰值。
第二,动态批处理调度器。把请求排队、Batch组装、优先级调度用异步任务实现,比多线程加锁的方式更直观。Rust的异步运行时调度比较高效,任务切换开销可控。
第三,单二进制部署带来的部署简化。训练环境常见多套Python虚拟环境、一堆动态链接库;推理服务用Rust打包后,一个静态二进制就能跑起来,这在灰度发布和边缘节点部署上非常省心。尤其是CPU推理场景、边缘服务器、嵌入式设备,这类环境往往没有干净的Python运行时,Rust单二进制的优势会被放大。
我了解到某团队的CPU版量化推理服务,就是拆掉原来C++服务的一部分算子调度逻辑,用Rust重写后嵌入原链路。最终不仅内存峰值下降了,部署镜像体积也缩小了一个量级。这个案例的参考价值在于:他们并没有推翻整个服务,而是把最敏感的那一层换成了Rust。
3. 信号三:AI调度器与数据管线新项目从第一天就用Rust
3.1 为什么新基建项目更倾向“从零Rust”
如果说前两个信号是“存量系统换血”,那第三个信号更彻底:很多AI基建的新项目在立项那一刻就选了Rust,没有历史包袱。调度器、特征管道、数据摄取、监控采集这些组件,过去用Python写原型很方便,但一旦要支撑生产级数据量,Python的解释器开销和GIL并发限制很快成为瓶颈。现在团队往往会先问一句:这项目上线后要扛多少QPS和吞吐?如果预期很高,直接上Rust反而省得后期重写。
这里的关键不是“Rust比Python快”,而是“Rust的工程形态更适合当基建底座”。基建组件生命周期长,要长期演进,还要被其他团队依赖。选型时更看重稳定性、可维护性和性能上限。Rust的强类型系统和显式错误处理能保证大规模重构时更容易发现问题,单二进制产物也大幅降低了交付和运维成本。
举一个数据管线的例子。某团队做特征数据回放管道,原实现用Python加多进程管理。数据量一上去,进程数变成几十个,内存占用暴涨,而且偶尔出现子进程卡死、数据重复消费的问题。后来他们用Rust重写了核心管道:流式读取、解析、过滤、窗口聚合全部在一条异步链路上完成。同样的吞吐下,内存峰值大概降到原来的一半,回放耗时为原来的六分之一左右,而且进程模型简单了,部署和排查都轻松不少。
3.2 调度与数据管线中真正省心的地方
Rust在调度与数据管线里的收益,不只在性能上,还有工程层面的:
- 编译期就能发现逻辑错误,像配置字段拼错、状态枚举漏处理这类低级问题,运行期几乎不会出现。
- 错误处理强迫显式化,任何一步失败都必须决定是传播、重试还是降级,不会像异常那样容易悄悄丢失上下文。
- 交叉编译到ARM平台非常方便,训练中心用x86,边缘推理机是ARM,一套代码直接出两种产物。
- 静态链接产物在容器镜像里非常干净,不需要额外安装一堆运行时依赖。
这对平台基础设施团队吸引力很大。基建组件往往是“整体价值大但单点功能琐碎”,如果一门语言能减少运行故障、降低交付复杂度,那它对人力资源的节省远大于初期学习成本。
3.3 团队配置上怎么过渡
从零选择Rust还有一个现实原因:新建一个基建组件时,核心开发只是少数几个人,团队完全可以用两到四周时间完成语言学习,再用一个中等规模模块试水。只要核心成员对系统编程有经验,Rust的上手曲线没有传说中那么陡。更稳的做法是先招或培养一位对所有权和生命周期有清晰理解的技术负责人,把安全的边界模型定下来,其余成员按模块分工。最忌讳的是整个团队边学边写一个大型分布式系统,那会把编译错误变成团队摩擦。
如果组织里已经积累了Python生态的算法代码,也不必强制全部重写。更常见的是双语言架构:Rust负责数据密集和并发敏感的底座,Python负责模型调用和业务编排,两边通过一系列高吞吐接口连接。这种组合在AI基建里正在变成一种新常态。
4. 给AI工程师的实际操作建议:在不大改业务代码的前提下吃掉Rust红利
4.1 三种接入路径怎么选
不是每个人都应该立刻把整套系统用Rust重写。我总结了三类接入路径,按侵入性从小到大排:
- 写Rust命令行工具:把数据清洗、格式转换、批量评估这类一次性任务做成Rust CLI,直接替代原来的Python脚本。这是成本最低的切入点。
- 用Rust写高性能模块,通过C ABI提供接口,Python侧用标准库的共享库加载机制调用。适合把特征计算、相似度计算、解析逻辑等热路径替换掉。
- 用Rust重写完整服务,独立部署,只对外提供HTTP或消息接口。适合推理服务、向量检索这类独立组件。
路径1几乎无风险,路径2能立刻看到收益,路径3则需要更完整的技术评估。我建议多数团队先从路径2入手,因为它贴近现实:算法和业务代码继续用Python,只有性能瓶颈模块用Rust处理,风险边界很清晰。
4.2 实例:用Rust给Python写一个余弦相似度模块
这里用一个最常见的操作做演示:给Python提供一个Rust实现的余弦相似度计算函数。之所以选它,是因为向量相似度在检索、特征比对、模型评估里高频出现,而且它是典型的CPU密集操作,非常适合验证Rust插件的效果。
先写Rust侧代码,导出一个C ABI函数:
#[unsafe(no_mangle)] pub extern "C" fn cos_sim( data_a: *const f32, data_b: *const f32, len: usize, ) -> f32 { if data_a.is_null() || data_b.is_null() || len == 0 { return 0.0; } let a = unsafe { std::slice::from_raw_parts(data_a, len) }; let b = unsafe { std::slice::from_raw_parts(data_b, len) }; let mut dot = 0.0f64; let mut norm_a = 0.0f64; let mut norm_b = 0.0f64; for i in 0..len { let av = a[i] as f64; let bv = b[i] as f64; dot += av * bv; norm_a += av * av; norm_b += bv * bv; } if norm_a == 0.0 || norm_b == 0.0 { return 0.0; } (dot / (norm_a * norm_b).sqrt()) as f32 }关于这段代码有两个设计点。第一,我没有直接在f32上累加,而是提升到f64,这是为了避免大量浮点数累加时精度损失过大。第二,参数用的是裸指针,因为在C ABI边界上Rust的所有权无法直接传递,需要在函数内部转成安全的切片引用。这是FFI边界上的常见写法,接口很小,风险可控。
编译成共享库:
rustc --crate-type cdylib -O cos.rs -o libcos.so再用Python调用:
import ctypes import array import random lib = ctypes.CDLL("/path/to/libcos.so") lib.cos_sim.argtypes = [ ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float), ctypes.c_size_t, ] lib.cos_sim.restype = ctypes.c_float vec_a = array.array("f", (random.uniform(-1, 1) for _ in range(512))) vec_b = array.array("f", (random.uniform(-1, 1) for _ in range(512))) ptr_a = ctypes.cast(vec_a.buffer_info()[0], ctypes.POINTER(ctypes.c_float)) ptr_b = ctypes.cast(vec_b.buffer_info()[0], ctypes.POINTER(ctypes.c_float)) score = lib.cos_sim(ptr_a, ptr_b, len(vec_a)) print(score)这个实现能跑,但必须注意到一个关键问题:如果每次计算只传一组长度为512的向量,FFI调用的固定开销可能比计算本身还大,性能反而更差。正确做法是批量处理,一次传入几千条向量的连续内存,让批内计算摊薄调用开销。这也是“Rust模块要设计成批量接口”的核心原因,接口越粗,收益越明显。
4.3 更工程的封装方式与性能注意事项
C ABI适合快速验证,但正式项目里更推荐使用成熟的绑定生成工具和模块封装方案。Python侧可以通过官方扩展模块方式嵌入Rust逻辑,这样调用开销更低,还能直接传递Python对象和异常信息。不过这会引入更多构建链路配置,我建议先把C ABI这条路走通,理解FFI边界的行为,再决定要不要上更深的集成。
接入Rust模块时,有三个性能问题要提前规划:
- 数据拷贝能避免就避免。尽量让Rust直接读取Python buffer协议暴露出的连续内存,不要先转成列表再传。
- 用多线程时要注意数据竞争。Rust侧可以安全地并行处理多个独立批次,但要把任务切分清楚,避免引入锁竞争。
- 热路径上不要做频繁的Rust到Python往返。一次调用做尽可能多的计算,返回一个聚合结果,远好于循环里调几千次小函数。
5. 踩坑实录:Rust落地AI基建的真实问题与排查思路
5.1 编译链路变长,如何保住迭代速度
Rust的编译速度一直是被吐槽的重灾区。一个中大型项目在Release模式下全量编译动辄几分钟,加上依赖树一起编译,等待时间更长。这对AI场景很伤,因为算法工程师习惯了Python改一行立刻能跑。
我的应对方式是三层组合。第一层:Debug模式和增量编译只用于逻辑自测,性能测试必须走Release。第二层:把大型项目拆成多个工作区成员,让变更尽量限制在单一模块,避免全量重编。第三层:在持续集成里预先构建并缓存依赖,让本地提交只编译业务代码,显著缩短反馈周期。
还有一个小技巧:给与算法团队交互的模块保持最小依赖,不要让一个计算函数拖进来几百个间接依赖。
5.2 跟底层C/C++库绑定,怎么少流血
AI基建绕不开GPU算子、矩阵库、各类传统数据处理库。Rust虽然性能和C++接近,但生态覆盖度和C++相比仍有差距,某些能力必须通过FFI绑定到底层库。这里的坑在于:底层库往往是C++接口,不是纯C接口,而Rust的FFI默认只对C ABI友好。直接绑C++需要额外的抽象层,常见做法是在底层C++库外面再包一层C接口,再让Rust这一侧对接。
绑定过程中最需要注意的是生命周期问题。底层库返回的指针,到底是库持有还是调用方持有?释放责任在哪一侧?如果文档没写清楚,很容易在Rust侧包了一层安全接口后发现还是在泄漏。我习惯在接入任何底层库之前,先写一个最小测试确认分配和释放路径,确认无误后再扩展开来。
另外,Rust侧应尽可能把不安全代码收敛到很小的模块里,通过安全API暴露给上层。不要让不安全代码渗透到业务逻辑中。
5.3 别踩异步和CPU密集任务的“假并发”坑
Rust的异步运行时非常擅长处理大量IO并发,但很多AI计算任务是CPU密集的。如果在一个异步任务里做长时间CPU计算,可能会阻塞执行器线程,导致整个服务的请求调度受影响。这不是说Rust不能做CPU密集服务,而是要把CPU密集任务放到专门的线程池或阻塞任务机制中,避免和异步IO任务混跑。
我在接手一个推理服务时踩过这个坑。最初把模型推理直接放在异步任务里,压测时发现某个大请求进来后,后面的健康检查请求全部被堵住。排查后确认是CPU计算占住了执行器线程。把推理调用迁移到独立线程池后,问题立刻消失。
5.4 什么时候暂时别上Rust:场景速查表
同样重要的,是知道哪些场景不要硬上Rust。我总结了一张速查表:
| 场景 | 建议 |
|---|---|
| 科学计算快速原型、探索性研究 | 继续用Python,Rust迭代成本太高 |
| GPU算子底层开发 | 以C/C++生态为主,Rust做外围控制层 |
| 简单后端CRUD、表单API | Go或Java更高效,没必要上Rust |
| 延迟敏感、内存敏感的在线推理与检索 | Rust非常值得考虑 |
| 生命周期长的数据管线和调度底座 | Rust的单二进制和稳定性很加分 |
| 团队完全没有系统编程经验 | 先做一个独立模块练手,再扩大范围 |
把“用Rust”当成目的本身,是我最不建议的路径。它应该是在延迟、稳定性、安全、部署成本等多个约束下自然胜出的结果。
6. 最后说一下我自己的取舍体会
我最初接触Rust时,也觉得它的学习成本偏高。真正用起来之后,我发现这东西带来的收益不在“写的时候有多爽”,而在“上线后有多省事”。所有权模型把很多原来要在运行期靠监控、日志和偶然复现才能发现的问题,直接变成了编译错误。编译错误虽然当时让人烦躁,但它便宜又快速。
在AI基建这波转向里,我一直告诉自己一个原则:不要因为Rust热就无脑选它,也不要因为它有学习成本就完全拒绝。关键是找到那些值得用Rust硬碰硬的环节,比如向量索引、KV Cache管理、特征管道、推理调度。这些位置一旦出问题,影响的是整个系统的稳定性和成本,很值得用更底层的语言去换取可控性。
如果你也想尝试这条路线,我的建议非常具体:先别大规模重写,挑一个延迟敏感但逻辑足够独立的小模块,用Rust重写并满足现有接口。跑一段时间对比稳定性、延迟和部署代价。做好这一步,你自然能获得属于自己的判断,而不是听别人说“Rust很好”或者“Rust很难”。实际用数据说话,永远是最可靠的决策方式。