为什么我们要用 Rust 重新写一个推理引擎
zLLM 为什么选择以 Rust 为主体,从模型执行、资源生命周期和硬件能力出发,重新设计一个原生推理引擎。
zLLM 的定位不是给既有框架增加一个 Rust 外壳,而是以 Rust 为主体、以最少且必要的第三方依赖,从模型执行、数据生命周期和硬件资源出发,重新设计一个原生推理引擎。
这篇文章回答三个问题:zLLM 为什么必须拥有自己的引擎边界,为什么 Rust 恰好适合表达这些边界,以及这种选择如何落实到真实代码与完整推理路径中。
今天再写一个大模型推理引擎,很容易被问到两个问题:为什么不用已经成熟的引擎?为什么偏偏选择 Rust?
如果目标只是尽快让一个模型跑起来,复用现有框架通常是更经济的选择。但 zLLM 要解决的不是“把模型跑起来”这一件事。我们希望同一套引擎能够面对 Apple UMA、CPU、CUDA、ROCm、Vulkan、NPU、本地 SSD、大模型 MoE 权重、长上下文 KV Cache,以及未来多机分层执行。这些资源有不同的存储位置、同步方式、带宽、生命周期和失败边界。真正困难的部分,不是再封装一次算子 API,而是让整个推理过程的资源关系足够清楚,清楚到可以被验证、优化和长期演进。
因此,zLLM 选择从头设计。不是因为现有项目不够好,也不是为了追求“自研”本身,而是因为我们想要的边界与许多现有系统的历史边界不同。
我们所说的“从头写”是什么
从头写不等于拒绝标准,也不等于重复制造所有基础设施。
zLLM 继续读取 Safetensors、GGUF/GGML、compressed-tensors 和 ModelOpt 等标准权重格式,继续使用 Metal、CUDA、ROCm、Vulkan 等平台能力,也会采用经过验证的通用 Rust 库。我们不重新发明文件格式、网络协议或操作系统接口。
我们重新设计的是推理引擎本身:
- 模型规格如何表达;
- 模型算法如何只写一份;
- backend 应该提供哪些能力;
- kernel 如何保持参数化而不绑定模型;
- 权重、KV Cache、activation、expert 和 scratch 分别由谁拥有;
- prefill、append prefill 和 decode 的完整执行路径如何调度;
- 单机、嵌入式服务和多机分层如何共享同一套语义;
- 正确性如何由 CPU reference 和跨后端一致性验证锚定。
换句话说,zLLM 复用开放标准与平台能力,但自己拥有模型执行和资源语义:
| 复用 | 自己拥有 |
|---|---|
| Safetensors、GGUF/GGML、compressed-tensors、ModelOpt | 模型规格、算法编排与权重装配 |
| Metal、CUDA、ROCm、Vulkan、NPU 平台接口 | backend capability、资源驻留与同步 |
| 成熟的序列化、网络和异步 Rust 库 | KV、Expert、Activation、Scratch 的生命周期 |
| 平台原生 GPU kernel 语言 | 完整 Prefill、Append Prefill 与 Decode Round |
这也是“重新设计”与“重写”的区别。重写容易把旧结构换一种语言再实现一次;重新设计则要求回到数据依赖、资源位置和真实任务,重新判断每一个模块是否有存在的必要。
最小引擎,不是最少功能
zLLM 追求的“最小”,不是做一个只能演示的小程序,而是让系统中的概念数量保持最小。
我们的长期准则是 Simple / Short / Straight:
- Simple:能由一个清楚模块解决的问题,不拆成多层抽象;
- Short:代码、作用域和对象生命周期都尽可能短;
- Straight:定义靠近使用点,控制流和依赖方向可以顺着代码直接读懂。
目录、文件、trait、wrapper、manager、registry 和 context 都不是免费的。每增加一个实体,读代码的人就要多记住一个名字、多理解一层转发、多追踪一段生命周期。只有当一个实体确实拥有独立的数据、行为、生命周期或依赖边界时,它才值得存在。
所以 zLLM 不建立没有真实实现的通用 flow,不为了“未来可能有用”提前设计抽象工厂,也不保留只做转发的空壳 facade。模型执行流程只有一份;平台差异由 capability trait 和具体 backend 承接;kernel 只表达算子,不知道模型名字。
最小化的对象不是能力,而是理解系统所需的心智负担。
最少且必要的第三方依赖
“最小第三方依赖”同样不意味着零依赖。密码哈希、序列化、异步运行时、HTTP、标准格式解析等成熟能力,没有理由为了形式上的纯粹重新实现。
我们的原则是:依赖可以提供通用能力,但不能替我们拥有推理引擎的核心语义。
模型执行、资源驻留、KV 生命周期、权重装配、调度策略和跨后端能力边界,应当在 zLLM 自己的代码中清晰可见。第三方库不应在不透明的内部替我们决定张量放在哪里、什么时候复制、什么时候同步、如何回收,或者某个模型如何执行。
这带来三个直接收益:
第一,依赖图更容易审计。升级一个网络库,不应改变模型数值;增加一种权重格式,不应侵入 runtime;替换某个平台绑定,不应迫使模型算法重写。
第二,性能成本更可见。推理系统最怕“方便的抽象”背后出现一次隐式拷贝、一次设备同步或一次临时反量化。核心路径由自己拥有,才能追踪每一份大数据的来源、去向和生命周期。
第三,长期演进不受上游框架结构限制。zLLM 可以复用标准和通用库,但不会把自己的架构方向交给某个大型运行时、张量框架或 Python 扩展体系决定。
为什么 Rust 适合这件事
Rust 的价值不只是“没有 GC”或“比 Python 快”。对于推理引擎,Rust 最重要的优势是:它能把资源关系写进程序结构,并在编译阶段消灭大量不合法状态。
1. 所有权与生命周期正好对应推理资源
推理引擎处理的不是抽象数字,而是一组昂贵资源:mmap 权重、设备 buffer、KV page、command buffer、异步 I/O、跨节点 session、临时 scratch。它们都在回答相同的问题:谁拥有、谁可以借用、什么时候能够释放、异步任务结束前是否仍然有效。
Rust 的所有权、借用和生命周期不是额外约束,而是这类问题的直接语言表达。一个异步预取任务不能引用已经释放的目标 buffer;一份 cache 不能在仍被执行队列使用时被回收;共享状态必须明确同步边界。这些错误在 C/C++ 中往往依赖约定和审查,在带 GC 的语言中又容易被延迟释放或外部资源生命周期绕开。Rust 让其中相当一部分成为编译问题。
2. 无 GC 的可预测执行
decode 是持续、细粒度、对尾延迟敏感的循环。不可预测的暂停、隐式对象分配和延迟析构会让性能分析变得困难。
Rust 没有垃圾回收器,值的生命周期通常由作用域决定。配合预分配和 scratch 复用,我们可以让热路径中的分配、释放和同步更可预测。这并不自动保证高性能,但它提供了构建可预测系统所需要的基础。
3. 零成本抽象适合表达 backend 能力
不同 backend 的差异是真实存在的。CPU tensor 可以是普通内存,Metal tensor 是设备资源句柄,ROCm 可能有完全不同的驻留与提交模型。把它们强行塞进统一的动态对象,往往只会得到最低公分母,以及遍布热路径的间接调用和运行时检查。
zLLM 使用 trait 和 associated type 表达能力:模型 runtime 声明自己需要什么,具体 backend 在编译期提供实现。泛型会带来编译时间和二进制体积成本,但换来的是静态组合、可读的能力集合,以及热路径上更少的动态分派。
4. 枚举与模式匹配让格式状态显式
权重可能是 F32、F16、BF16,也可能保持 FP8、FP4、W4A16 或 GGUF block 编码直到 kernel。用明确的 enum 表达这些状态,可以要求每个 backend 对每种合法输入作出处理或明确拒绝,而不是在字符串、指针和隐式约定之间猜测。
这对量化尤其重要:量化不是“加载时先转成浮点”的附属功能,而是与模型规格、平台 kernel 正交的一条数据路径。状态越显式,隐式展开和错误分派就越难混入系统。
5. Rust 同时覆盖底层与服务层
许多推理系统天然分裂为两部分:底层由 C/C++ 或 GPU 语言实现,上层由 Python、Go 或另一套服务框架组织。边界两侧各自合理,但组合后往往出现重复的数据模型、跨语言 FFI、两套错误处理和难以统一的生命周期。
Rust 可以同时承担权重解析、CPU reference、backend 绑定、runtime、调度器、HTTP/SSE 服务和嵌入式库接口。GPU kernel 仍然使用平台原生语言——Metal Shading Language、HIP/CUDA、WGSL 或 AscendC——但 kernel 之外的引擎主体由一套类型系统、一套错误模型和一套资源语义贯穿。
这才是 zLLM 所说的“纯 Rust”:不是否认平台原生 kernel 的存在,而是不依赖 Python 控制面、不把核心执行委托给另一个大型张量运行时,Rust 直接拥有整个推理引擎。
6. 工具链让跨平台工程保持一致
Cargo 的构建、feature、测试和依赖管理,使 CPU、Metal、CUDA、ROCm、Vulkan 等路径可以在同一个工程中保持清晰边界。平台能力按 feature 和目标系统启用,CPU 单元测试不依赖真实权重,GPU 路径则由 CPU oracle 做数值锚定。
Rust 不能替代真机测试,也不能在编译期发现运行时编译的 MSL/HIP kernel ABI 错误。但它能把平台相关的不安全边界压缩在较小范围内,让其余绝大部分系统继续享受静态检查。
zLLM 的架构如何体现这些选择
zLLM 把系统分成几个职责明确、依赖单向的区域:
这里最关键的不是目录长什么样,而是依赖方向:
- 模型规格不依赖平台;
- 模型算法只写一份,不复制到各个 backend;
- backend 和 kernel 不知道具体模型;
- attention、MoE、KV Cache 等领域层保存设备无关语义和 reference;
- 权重层只负责标准格式、命名、shape 和生命周期,不接管模型算法;
- CPU 是正确性 oracle,GPU kernel 是经过一致性验证的加速实现。
这种设计避免了最常见的组合爆炸:不是每增加一个模型,就为每个平台复制一套执行流程;也不是每增加一个后端,就重新理解所有模型。新增模型主要增加规格、编排和权重适配;新增 backend 主要实现已有 capability;只有出现新的通用算子语义时,才扩展领域能力。
从数据流,而不是语义标签出发
从头设计给了我们一个重要机会:按真实数据依赖组织系统,而不是照搬论文章节或模型名词。
如果 A 的输出是 B 的输入,它们就是必须串行的 pipeline,应当靠近并共同管理中间状态。如果 A 和 B 独立读取 hidden,它们就应当分开,让 backend 有机会并行调度。权重的结构、kernel 的融合和资源的生命周期都遵循同一原则。
同样,生产推理只承认三类完整任务:新会话 prefill、已有会话 append prefill,以及完整 decode round。截断到某一层、只跑一个 kernel、只比较局部输出,都可以是诊断手段,但不能替代端到端任务。优化必须最终回到 TTFT、decode latency、吞吐、内存峰值、SSD 等待和设备利用率。
这让系统不容易被漂亮但无关的局部数字带偏。
Rust 不会自动带来正确性和性能
选择 Rust 不是宣称语言可以替代工程纪律。
Rust 无法证明 GPU kernel 的数值正确,无法阻止错误的算法设计,也无法保证一个看起来优雅的抽象没有性能代价。多 backend 意味着同一 kernel 语义可能需要 Rust reference、MSL、HIP、CUDA、WGSL 或 AscendC 多份高性能实现;泛型会增加编译成本;capability trait 也可能在缺乏约束时不断膨胀。
所以 zLLM 把以下纪律与语言能力放在一起:
- 新算子先有 reference 语义和单元测试;
- CPU 与设备 backend 做一致性验证;
- kernel 维度全部参数化,不写入模型常量;
- 不用孤立 kernel benchmark 代替完整路径指标;
- 没有测量证据,不为性能增加复杂度;
- 源码、编译通过、oracle 通过和真机端到端验证必须明确区分;
- 只做完成当前任务所需的最小改动,不借机重构无关代码。
Rust 提供的是一块坚实地基。真正决定引擎质量的,仍然是清楚的边界、可复现的验证和对复杂度的持续克制。
为什么值得从头开始
从头写的代价很高。权重格式要自己理解,kernel 要逐个平台优化,服务与调度要自己打磨,错误也没有上游框架替我们兜底。短期看,这条路一定比接入一个成熟运行时更慢。
但它换来了一个长期价值:系统中的每一个关键决定都有明确所有者。
当模型变了,我们知道应该修改规格、编排还是领域能力;当平台变了,我们知道应该修改 backend 还是 kernel;当性能下降,我们能够沿着权重、activation、KV、expert、scratch 和同步点逐一追踪;当系统扩展到多机,我们传输的是层边界 activation,而不是把模型内部状态和远端 RPC 混成新的热路径。
这就是 zLLM 的目标:不是成为依赖最庞大、抽象最完整的通用 AI 框架,而是成为一个足够小、足够直接、能够真正拥有硬件资源和模型执行的原生推理引擎。
我们选择 Rust,因为它与这套目标一致:安全,但不放弃底层控制;抽象,但不必支付运行时成本;跨平台,但不抹平平台差异;能写 kernel 周围最靠近硬件的代码,也能一路写到服务和分布式调度。
我们选择从头设计,因为只有这样,Simple、Short、Straight 才不是对旧系统的局部修补,而能成为整个引擎从第一行代码开始遵守的结构原则。
