为什么我们要编写自己的C和C++推理引擎

Lobsters Hottest 工具

摘要

LocalAI 解释了为什么它要编写自己的 C/C++ 推理后端,并展示了其 vllm.cpp 移植版本在多个模型和硬件上的基准测试中,能够实现与 vLLM 相当或更高的吞吐量,同时占用空间远小于 vLLM。

<p><a href="https://lobste.rs/s/t7zdif/why_we_write_our_own_c_c_inference_engines">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/07/31 16:49

来源:https://localai.io/blog/why-we-write-our-own-engines/ 大多数 LocalAI 后端都是包装其他人的引擎,而这是正确的默认做法。llama.cpp、vLLM、whisper.cpp、stable-diffusion、MLX 等由比我们更擅长这些模型的人维护,包装它们只需要一个 Dockerfile 和一个 gRPC 垫片。 我们的 18 个后端不包装任何东西。它们是我们从头编写的 C 或 C++ 移植版,每一个之所以存在,是因为包装上游引擎意味着我们要交付一些无法交付的东西:多 GB 的 Python 安装、不可移植的 CUDA-only 堆栈,或者根本没有 C++ 实现的模型。这篇文章讨论的是这些移植版能带来什么(经过实测),以及它们代价是什么。 ## 你得到的是:一个文件,以及可预测的内存 部署 Python 推理堆栈意味着在安装时、在目标机器上、针对它拥有的任何 CUDA 和 glibc 解析依赖树。部署 ggml 移植版意味着复制一个共享库和一个 GGUF 文件。 这种差异最清晰的度量是 vllm.cpp (https://github.com/mudler/vllm.cpp),这是我们用 C++20 移植的 vLLM V1 服务架构。安装 vLLM 会产生一个 9.1 GiB 的 virtualenv。安装 vllm.cpp 会产生一个 66 MiB 的二进制文件。该引擎实现了 Python 原版所做的相同事情,包括分页 KV 缓存、连续批处理、前缀缓存、调度器和采样器,推理时没有 Python、没有 PyTorch、也没有 ggml。 显而易见的问题是这在吞吐量上要付出什么代价。在 NVIDIA GB10 上运行 Qwen3.6-27B,使用 NVFP4,贪婪解码,闭环测试,与 vLLM 在其生产环境的 graph 配置(而非 `--enforce-eager`)下对比: | 并发度 | 1 | 2 | 4 | 8 | 16 | 32 | |---|---|---|---|---|---|---| | **vllm.cpp** tok/s | **86.05** | **159.68** | **292.34** | **508.77** | **801.76** | **1095.01** | | vLLM tok/s | 82.32 | 158.03 | 290.31 | 505.46 | 789.16 | 1076.25 | | 比率 | 1.045x | 1.011x | 1.007x | 1.007x | 1.016x | 1.017x | 我们在全部六个点上领先,而且六个中有五个是平局。我们运行间的噪声带是 0.5%,并发度 2 到 32 落在 0.7% 到 1.7% 之间,所以诚实的解读是只有单流场景(4.5%)明显超出噪声。输出在该曲线上的每个点都与 vLLM 逐 token 完全相同。峰值主机内存为 24.88 GiB,而 vLLM 为 28.18 GiB。 对于一个 66 MiB 的二进制文件来说,与成熟的 CUDA 堆栈打平已经是不错的结果,而且这意味着内存占用上的节省并没有用吞吐量来偿还。与 llama.cpp 在 CPU 上运行同一个 GGUF 文件相比,prefill 快 1.18 倍(223.8 对 177.3 tok/s),decode 在 llama.cpp 自身的波动范围内是平局,而且 token 与其贪婪解码逐字节相同。在 Apple M4 上对比 MLX-LM,prefill 的首 token 时间领先 1.5%,热态总吞吐量是 MLX-LM 的 97.6%,这是一个真实存在的 2.4% 差距,完全位于 decode 中。 ## 有时移植版就是更快 depth-anything.cpp (https://github.com/mudler/depth-anything.cpp) 是字节跳动 Depth Anything 3 的移植版,它可以从一张普通照片给出以米为单位的度量深度,以及逐像素置信度、相机内参和外参,以及反投影点云。在 CPU 上,它比运行同一模型的 PyTorch 更快。 | 引擎 | 量化 | 模型 MB | 加载 ms | 推理 ms | 峰值内存 MB | 相对 PyTorch | |---|---|---|---|---|---|---| | PyTorch | f32 | 516 | 749 | 4916.9 | 1328 | 1.00x | | **C++/ggml** | q8_0 | 142 | **40** | **319.4** | **363** | **1.31x** | 同一模型,速度 1.31 倍,内存 27%,加载在 40 ms 内完成而不是 749 ms,测试使用的是 Ryzen 9 9950X3D,分辨率 504x336,16 线程。量化 q4_k 构建是一个 99 MB 的文件,并且保持接近无损。在 37 项一致性测试中,输出与参考前向传播逐分量相关性为 1.0。 它更快的原因与写出比 PyTorch 更好的 matmul 内核无关。两个位置嵌入——DPT 头部的 UV 嵌入和主干网络的双三次位置嵌入——在每次前向传播中都会用单线程标量 sin、cos 和双三次循环重新计算,尽管它们只依赖于输入几何形状,并且每次调用都是相同的。缓存它们每次前向移除了约 95 ms 的主机端开销,这占了差距的大部分。PyTorch 使用向量化操作构建相同的嵌入,从未付出这一成本。 这些胜利大体上就是这样的形态。重型 GEMM 几乎打平,因为每个人都在调用同一类 BLAS 内核。差异在于 Python 参考实现从未费心优化的主机端工作,以及不加载解释器和框架来做推理。在 GPU 上,情况又回到平局:使用 ggml CUDA 后端和 GB10 上的 flash attention,depth-anything.cpp 与 PyTorch 调优后的 cuDNN 打平,每次前向 47.3 ms,只在冷启动上胜出,加载快 1.75 到 2.9 倍。 ## 一致性是门槛,速度是后续 face-detect.cpp (https://github.com/mudler/face-detect.cpp) 和 voice-detect.cpp (https://github.com/localai-org/voice-detect.cpp) 取代了 LocalAI 的 Python `insightface` 和 `speaker-recognition` 后端。两者都是我们不声称 CPU 速度优势的情况,而且两者还是发布了。 face-detect.cpp 运行完整的 insightface buffalo 链,也就是 SCRFD 检测、五点相似变换对齐到 112x112,以及 ArcFace 嵌入,全部来自一个自包含的 GGUF,没有 Python,没有 onnxruntime。检测器边界框和关键点与 insightface 匹配在 1 像素以内,识别嵌入的余弦相似度为 1.000000,在任何线程数下都保持。在 CPU 上它比 onnxruntime 慢:SCRFD 检测在单线程约 0.83x,八线程约 0.69x,ArcFace 嵌入约 0.61x 和 0.84x。onnxruntime 的 MLAS 卷积内核处于 FMA 端口峰值,自定义 AVX2 Winograd 路径缩小了差距但未能完全追上。在

相似文章