资讯动态

vLLM底层解析:C++如何撑起大模型高性能推理

发布时间:2026/8/30 15:29:34 来源:尧图企业网站定制
最近在大模型推理落地时我经常被问到同样一个问题vLLM 不是 Python 框架吗为什么还要聊 C为什么社区里偶尔会看到 “C Version of vLLM” 之类的讨论其实这个疑问很自然。我们用 vLLM 部署模型时写的是 Python API调的是LLM()或者AsyncLLMEngine表面看起来就是一个纯 Python 项目。但如果深入看 vLLM 的源码和构建产物会发现大量 C 与 CUDA 代码包括自定义算子、显存管理、注意力内核等。也就是说vLLM 的“调度大脑”是 Python但真正决定性能的“计算手脚”是 C/CUDA。本文会围绕“C 与 vLLM”这个主题展开讲清楚vLLM 架构中 Python 与 C 的边界为什么高性能推理引擎需要 C用 C libtorch 实现一个最小推理算子的完整流程通过 PyTorch C Extension 为 vLLM 类项目编写自定义算子常见编译错误与生产环境最佳实践。无论你是想了解 vLLM 的底层原理还是打算用 C 做推理引擎二次开发这篇文章都能提供一条可以实操的路径。1. 从 vLLM 谈起为什么高性能推理离不开 C先来看一个最简单的例子。用 vLLM 启动一个 Qwen 系列模型时我们通常只需要几行 Python 代码。但在这几行代码背后模型权重经过加载、量化、切分、图优化后会变成一系列 CUDA Kernel 在 GPU 上执行。这些 Kernel 以及相关的高性能算子库恰恰是用 C 和 CUDA 编写的。1.1 vLLM 是什么vLLM 是一个面向大语言模型LLM的高吞吐量推理引擎核心优势是PagedAttention通过类似虚拟内存的分页管理提高 KV Cache 利用率Continuous Batching请求级别动态调度避免静态 batch 的等待浪费高效算子融合了 Attention、激活、量化等计算减少显存读写和 Kernel 启动开销。可以把它理解成“大模型部署领域的数据库引擎”对外提供高层的查询接口对内做执行计划、内存管理、并发控制和底层算子优化。1.2 Python 是外壳C 是内核vLLM 的代码结构大致可以分为两层层级主要语言职责引擎层Python请求调度、批处理策略、KV Cache 管理、采样策略、API 服务算子层C、CUDA矩阵乘法、Attention Kernel、激活函数、量化操作、显存拷贝框架层PyTorch张量定义、自动求导基础、设备管理、模型权重加载也就是说vLLM 并不是“不用 C”而是把 C 封装在 Python 扩展中。用户通常看不到 C但编译 vLLM 时那一堆ninja、gcc、nvcc的日志已经说明了很多问题。1.3 为什么会有 “C Version of vLLM” 的讨论社区中出现这个讨论主要有几个原因学习与研究很多人想通过 C 重读 vLLM 核心逻辑理解注意力计算和显存管理到底怎么实现。低延迟部署Python 解释器、GIL 锁、对象分配在极端低延迟场景下会成为额外开销C 推理服务可以把单请求延迟压得更低。嵌入式/边缘设备很多边缘设备没有完整的 Python 运行环境或者 Python 环境过重C 静态编译更友好。算子融合与硬件适配为特定加速卡编写高性能算子时绕不开 C/CUDA 或 C/ROCm。需要说明的是目前并没有一个官方发布的、名为 “C Version of vLLM” 的替代项目。更准确的说法是vLLM 本身包含 C 组件同时社区有大量基于 C 的 LLM 推理引擎比如 llama.cpp 风格的引擎可以借鉴。2. vLLM 技术栈解析Python、CUDA 与 C 的边界为了在工程上做出合理决策我们有必要把 vLLM 的技术栈拆开看。2.1 调度与计算分离大模型推理引擎通常采用“调度与计算分离”的设计调度器Scheduler负责接收请求、判断当前 GPU 显存是否足够、决定何时执行哪个请求、何时释放 KV Cache。执行器Executor负责把调度结果转换成实际的计算任务调用底层算子完成 Prefill 和 Decode。vLLM 的调度器主要由 Python 实现原因在于调度逻辑复杂涉及优先级、显存水位、等待队列、抢占等策略Python 开发效率高调度频率远低于算子执行频率Python 的开销可以被计算时间覆盖。但计算层不能直接用 Python。矩阵乘法、Attention 这类操作如果循环调用 Python API一次前向就要产生成千上万次 Python 调用性能不可接受。2.2 自定义算子的存在形式vLLM 的算子扩展目录通常包含多个自定义算子例如paged_attention分页注意力核心实现activationSilu、GELU 等融合激活quantizationAWQ、GPTQ、FP8 等量化内核ops各种融合算子。这些算子在 Python 侧通过torch.ops或torch.library注册底层是 C/CUDA 编译出的动态库。用一个简单的 ASCII 图表示Python 调度层 │ │ 调用 torch.ops.vllm.xxx ▼ C 扩展层torch/extension.h 绑定 │ │ 调用 CUDA Kernel ▼ GPU Kernel 计算2.3 与 PyTorch、SGLang 的关系经常有初学者把 vLLM、SGLang、LangChain、PyTorch 放在一起比较简单区分PyTorch 是深度学习框架定义张量和自动求导的基础设施vLLM 是基于 PyTorch 构建的推理引擎专注于服务化场景的吞吐和显存效率SGLang 是另一个推理引擎提出了 RadixAttention 等思路目标也是高效 ServingLangChain 是应用层编排工具通常用于串联模型、提示词模板、外部工具。因此vLLM 与“PyTorch”不是对立关系而是依赖关系。vLLM 在 PyTorch 之上构建而 C 编写的部分常常直接使用 PyTorch 的 C 前端libtorch来管理张量。3. 用 C 实现 LLM 推理先掌握这些核心概念如果要自己用 C 实现一个“类 vLLM”的推理引擎或者阅读 vLLM 的 C 源码下面几个核心概念是绕不开的。3.1 张量库libtorch 扮演的角色C 开发中最麻烦的就是多维数组的存储、广播、设备切换。如果全部手写代码会非常啰嗦。所以多数 C 推理引擎会依赖一个张量库。libtorch 是 PyTorch 的 C 前端可以在 C 中直接创建torch::Tensor调用matmul、softmax、relu等算子并且能加载 PyTorch 保存的权重。这让“用 C 写推理”变得现实。示例创建一个随机张量并做矩阵乘法。#include torch/torch.h #include iostream int main() { torch::Tensor a torch::randn({2, 4}); torch::Tensor b torch::randn({4, 3}); torch::Tensor c torch::matmul(a, b); std::cout c std::endl; return 0; }运行时需要保证 CUDA 版本与 libtorch 一致否则会出现无法加载内核或符号缺失的问题。3.2 模型权重加载vLLM 加载模型权重时读取的是 safetensors 或 bin 文件。用 C 实现时可以选择使用 libtorch 的torch::pickle/torch::load加载 TorchScript 格式使用第三方库解析 safetensors在 Python 侧完成权重转换导出为自定义二进制格式再让 C 读取。工程上最稳妥的方式是先用 Python 把模型权重导出成一个结构清晰的二进制或 mmap 文件然后在 C 中按偏移量读取。这样做可以避免直接依赖复杂的 safetensors 解析逻辑。3.3 Attention 计算从标准实现到 PagedAttention 思路大模型推理的核心是 Attention。标准 Attention 公式可以简化为Attention(Q, K, V) softmax(Q K^T / sqrt(d_k)) V在 Decode 阶段KV Cache 不断增长。vLLM 的 PagedAttention 把 KV Cache 划分成固定大小的 block每个 block 可以存储多个 token 的 K/V。这样有效解决了显存碎片和预分配浪费问题。如果要用 C 实现类似机制核心数据结构是逻辑 KV 块表记录每个序列占用哪些 block物理显存池按 block 大小预分配查表函数将逻辑位置映射到物理地址。这个设计本质上是“显存空间的虚拟内存管理”适合在 C 中完成。3.4 连续批处理与调度状态机连续批处理Continuous Batching是指当一个请求生成完当前 token 后不再固定在 batch 中等待所有请求完成而是立刻换入新的请求。C 实现时需要维护每个请求的状态至少包含是否处于 Prefill 阶段已生成 token 数最大生成 token 数KV Cache 的 block 索引采样参数。调度器每次迭代时遍历状态列表挑选能执行的请求组装成一个 batch 的输入张量。4. 环境准备与构建配置在动手写代码之前先把编译环境准备好。4.1 推荐环境以常见 Linux 环境为例操作系统Ubuntu 20.04 / 22.04GPUNVIDIA 显卡驱动版本支持 CUDA 11.8 或 12.xCUDA Toolkit11.8 / 12.1 / 12.4 等与驱动匹配编译器gcc/g 9 或更高版本CMake3.18 或更高libtorch与 PyTorch 2.x 对应的 C 发行版。如果你使用的是 Windows理论上也能编译 libtorch 项目但需要注意 MSVC 版本和 DLL 路径。本文以 Linux 为主线。4.2 下载 libtorchlibtorch 可以从 PyTorch 官网获取。为了保持稳定建议选择与训练环境一致的版本。例如wget https://download.pytorch.org/libtorch/cu121/libtorch-cxx11-abi-shared-with-deps-2.1.0%2Bcu121.zip unzip libtorch-cxx11-abi-shared-with-deps-2.1.0cu121.zip -d /opt/这里选择的路径是/opt/libtorch下文 CMake 会引用它。实际版本请按自己的 CUDA 环境调整。需要说明的是安装包分为cxx11 ABI和pre-cxx11 ABI两种编译时一定要与编译器配置保持一致否则会报大量 ABI 相关链接错误。4.3 项目结构接下来创建一个最小 C 工程cpp_vllm_demo/ ├── CMakeLists.txt └── src/ └── main.cpp4.4 CMakeLists.txtcmake_minimum_required(VERSION 3.18) project(cpp_vllm_demo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 根据本机实际 libtorch 路径修改 set(TORCH_PATH /opt/libtorch) set(CMAKE_PREFIX_PATH ${TORCH_PATH}) find_package(Torch REQUIRED) add_executable(cpp_vllm_demo src/main.cpp) target_link_libraries(cpp_vllm_demo ${TORCH_LIBRARIES}) # 如果链接出现 pthread 相关错误可加上 Threads find_package(Threads REQUIRED) target_link_libraries(cpp_vllm_demo Threads::Threads)配置完成后mkdir build cd build cmake .. make -j$(nproc)如果一切顺利会在build目录下生成cpp_vllm_demo可执行文件。5. 实战用 C libtorch 实现一个最小推理算子下面我们实现一个演示程序模拟 LLM 推理中最常见的两个步骤对输入做 QKV 线性投影简化版 Attention 计算只做单头不含 mask 细节。这个示例不是一个完整模型而是演示“C 如何组织张量计算”代码基本可运行适合作为入门模板。5.1 编写主程序文件路径src/main.cpp#include torch/torch.h #include iostream // 模拟 QKV 线性投影x W b torch::Tensor linear(torch::Tensor x, torch::Tensor w, torch::Tensor b) { return torch::addmm(b, x, w.t()); } // 单头简化 Attentionsoftmax(Q * K^T / sqrt(d)) * V torch::Tensor simple_attention(torch::Tensor q, torch::Tensor k, torch::Tensor v) { int64_t d q.size(-1); auto scores torch::matmul(q, k.t()) / std::sqrt(static_castdouble(d)); auto weights torch::softmax(scores, /*dim*/-1); return torch::matmul(weights, v); } int main() { // 模拟输入batch1, seq_len4, hidden8 torch::Tensor x torch::randn({4, 8}); // 随机初始化 Q/K/V 投影权重 torch::Tensor wq torch::randn({8, 8}); torch::Tensor wk torch::randn({8, 8}); torch::Tensor wv torch::randn({8, 8}); torch::Tensor bq torch::zeros({8}); torch::Tensor bk torch::zeros({8}); torch::Tensor bv torch::zeros({8}); torch::Tensor q linear(x, wq, bq); torch::Tensor k linear(x, wk, bk); torch::Tensor v linear(x, wv, bv); std::cout Q shape: q.sizes() std::endl; std::cout K shape: k.sizes() std::endl; std::cout V shape: v.sizes() std::endl; torch::Tensor out simple_attention(q, k, v); std::cout Attention output shape: out.sizes() std::endl; std::cout First row of output: out[0] std::endl; return 0; }5.2 为什么用 addmm 而不是 matmultorch::addmm(b, x, w.t())完成的是x W^T b也就是线性层的常见形式。和先matmul再add相比addmm通常能减少一次 Kernel 启动同时更接近 PyTorch 中nn.Linear的实现方式。在真实模型中Q/K/V 通常是同一次投影计算完成再拆成三份这里为了演示拆分成了三组权重。5.3 编译与运行mkdir build cd build cmake .. make -j$(nproc) ./cpp_vllm_demo预期会输出类似内容Q shape: [4, 8] K shape: [4, 8] V shape: [4, 8] Attention output shape: [4, 8] First row of output: [ tensor values... ]因为使用了随机权重每次运行结果不同但 shape 信息是固定的。5.4 如何扩展成真实推理这个示例距离完整推理还差几层权重需要从模型文件加载而不是随机初始化需要多层 Transformer Block 堆叠需要实现 KV Cache 和位置编码Decode 阶段需要逐 token 生成并通过采样输出。不过核心计算模式已经具备Linear、MatMul、Softmax、Reshape。后续工程化时只需在现有结构上增加模块。6. 为 vLLM 类项目编写自定义 C 算子除了“用 C 重写整个推理引擎”更常见的做法是在 vLLM 中用 PyTorch C Extension 编写自定义算子。这样既能保留 Python 的开发效率又能把热点计算下沉到 C。6.1 一个最简单的自定义算子我们编写一个“两个张量相加后乘标量”的算子体会一下完整流程。文件路径csrc/fused_add_mul.cpp#include torch/extension.h torch::Tensor fused_add_mul(torch::Tensor a, torch::Tensor b, double scale) { TORCH_CHECK(a.sizes() b.sizes(), a and b must have the same shape); TORCH_CHECK(a.device() b.device(), a and b must be on the same device); auto c torch::add(a, b); return torch::mul(c, scale); } PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { m.def(fused_add_mul, fused_add_mul, Add two tensors and multiply by scale); }文件路径setup.pyfrom setuptools import setup from torch.utils.cpp_extension import CppExtension, BuildExtension setup( namevllm_cpp_demo, ext_modules[ CppExtension( namevllm_cpp_demo, sources[csrc/fused_add_mul.cpp], ) ], cmdclass{build_ext: BuildExtension}, )然后执行构建python setup.py develop在 Python 中调用import torch import vllm_cpp_demo a torch.tensor([1.0, 2.0, 3.0]) b torch.tensor([4.0, 5.0, 6.0]) out vllm_cpp_demo.fused_add_mul(a, b, 2.0) print(out) # tensor([10., 14., 18.])6.2 这个示例和 vLLM 有什么关系真实 vLLM 中自定义算子通常涉及更复杂的逻辑例如自定义 DataType如 FP8、INT8、INT4 量化格式融合多个操作减少显存读写接收torch::Tensor和自定义索引结构。但扩展的注册方式是一样的用PYBIND11_MODULE导出函数用BuildExtension编译最后在 Python 中像普通模块一样调用。有一点要注意vLLM 内部很多算子是 CUDA 扩展不是纯 C 扩展。如果算子需要运行在 GPU 上并且要追求性能通常要写.cu文件并在setup.py中通过CUDAExtension而不是CppExtension构建。这已经切换到 CUDA C 领域需要额外熟悉线程组织、共享内存和显存访问模式。7. 常见问题与排查思路C 编译和推理引擎运行过程中遇到的问题集中在环境、版本、内存和计算结果四个方面。下面把常见问题整理成表。问题现象常见原因解决思路CMake 找不到 Torch未设置CMAKE_PREFIX_PATH在 CMakeLists 中指定 libtorch 路径或通过-DCMAKE_PREFIX_PATH/opt/libtorch传入编译报undefined reference to torch::...ABI 不一致或未链接 Torch 库检查cxx11 ABI是否匹配确认target_link_libraries包含${TORCH_LIBRARIES}运行时报libtorch_cuda.so: cannot open shared object file运行时动态库路径缺失执行export LD_LIBRARY_PATH/opt/libtorch/lib:$LD_LIBRARY_PATHCUDA 版本不匹配libtorch 对应 CUDA 版本与驱动/环境不一致用nvidia-smi和nvcc --version检查重新下载对应版本 libtorchWindows 下运行找不到 DLLlibtorch DLL 不在可执行文件目录在 CMake 中把*.dll复制到生成目录注意力输出出现 NaN 或 InfSoftmax 前数值过大或 mask 处理错误检查d缩放系数、是否加了 attention mask、输入是否包含 NaN显存不足/OOMKV Cache 分配过大或未及时释放按请求估算最大 block 数设计显存池并对超过阈值的请求做排队或抢占Python 扩展 import 失败编译后的.so与 Python 版本不匹配保证使用同一个 Python 环境执行 setup 和 import昇腾或其他加速卡上算子不工作不同加速卡需要对应的 Kernel 后端检查是否安装了对应芯片的 backend 适配层算子层需要按硬件实现这里特别想说一下硬件适配问题。vLLM 早期主要面向 NVIDIA GPU随着生态扩大也出现了面向其他加速卡的适配层。对于昇腾 910B 这类 AI 加速卡能否通过 vLLM 启动 embedding 向量模型和 reranker 模型本质上取决于两个条件对应芯片是否有适配的 PyTorch 后端vLLM 中与芯片相关的 C/CUDA 算子是否被替换为兼容实现。如果只是模型权重格式相同但底层算子不支持启动时就会报错。这类问题通常需要查看官方硬件适配文档找到对应的torch_npu或同类插件并确认 vLLM 版本与插件版本的兼容关系。8. 最佳实践与工程建议从 C 推理引擎的角度下面这些建议可以帮助你少走弯路。8.1 分层设计不要把所有逻辑塞进一个文件建议按模块拆分model/模型结构包括 Transformer Block、Attention、MLPengine/调度器、请求状态机、采样器kernels/自定义 CUDA/C 算子memory/KV Cache 物理块管理serving/HTTP/gRPC 服务入口。分层的好处是每个模块可以独立测试和替换。比如想替换 Attention 实现时不需要改动调度逻辑。8.2 重视内存管理大模型推理的显存瓶颈很大程度上来自 KV Cache。C 实现时建议统一从显存池分配 block避免频繁cudaMalloc对 block 使用引用计数或空闲链表管理调度器定期回收已经结束的请求对应的 KV 块。8.3 异常处理和日志C 不像 Python 有清晰的 traceback一旦在 GPU 计算中出现非法内存访问定位成本很高。建议在每个重要阶段检查TORCH_CHECK关键步骤打印 Tensor shape、device、dtype使用std::cerr或日志库记录请求 ID、batch 大小、显存占用在 release 版本保留-g调试符号方便线上崩溃定位。8.4 安全、测试与生产变更如果推理引擎要接入生产环境应该遵循几条原则在包含模型服务、鉴权、接口层时先确认所有请求来源合法模型服务不要直接暴露在公网修改算子实现后必须用相同随机种子对优化前后输出做数值对比涉及显存分配策略、batch 调度策略的调整先在小流量环境灰度验证对模型权重文件做校验和检查避免加载损坏文件如果涉及删除旧模型、清理历史数据先备份再操作并确认回收策略符合最小权限原则。8.5 性能优化顺序性能优化不要一上来就调算子建议按顺序排查请求调度策略是否正确批处理大小是否合理KV Cache 命中率是否有大量显存浪费是否频繁进行设备间拷贝是否存在 Python 侧热点如下采样、预处理、后处理最后才深入 Kernel 层优化比如算子融合、共享内存使用、向量化加载。9. 总结与学习路线通过本文你已经看到了 vLLM 中 Python 与 C 的关系理解了为什么“C Version of vLLM”这类讨论并不奇怪并完成了一个可运行的 C 算子示例和一个 PyTorch C Extension 示例。对于想深入学习的人来说下一步可以按这个顺序继续熟悉 libtorch 的 Tensor API练习用 C 写 Linear、Softmax、LayerNorm阅读 vLLM 源码中csrc/目录下的算子命名和注册方式尝试用 C 复现一个简化版 KV Cache 管理模块如果想深入 GPU 优化再系统学习 CUDA 编程和注意力 Kernel 实现。C 推理引擎的工程量不小但它的性能收益和对底层硬件的掌控力是高级推理系统不可或缺的一部分。建议先从一个小算子开始动手不要一开始就试图重写完整引擎。只要跑通第一个 C 推理算子后续的思路就会清晰很多。如果这篇文章对你有帮助可以收藏备用。后续如果有人问到 vLLM 的自定义算子或 C 扩展也可以把这篇基础教程转给他。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价