资讯详情

推理引擎选型与vLLM调度机制:从PagedAttention到部署调优实战

📅 2026/11/3 16:31:56 | 华诺云谱 👁 阅读
推理引擎选型与vLLM调度机制:从PagedAttention到部署调优实战
推理引擎这个词这两年出现的频率越来越高但很多人第一次听到时的反应是这跟模型本身有什么区别我直接拿 transformers 跑不也能出结果吗能跑和跑得好是两码事。训练框架关心的是怎么把模型训出来推理引擎关心的是怎么把训好的模型用最低的成本、最快的速度、最稳的状态跑起来。这两件事的优化目标完全不同所以才会分化出 vLLM、SGLang、TensorRT-LLM、llama.cpp 这一整条技术路线。我自己是从 llama.cpp 开始接触推理引擎的后来因为要部署线上服务转到 vLLM中间踩了不少坑也花了很多时间去看它的调度逻辑和源码结构。这篇就把我对推理引擎这个领域的理解拆开讲一遍包括它到底解决什么问题、主流方案各自适合什么场景、vLLM 内部的 EngineCore 和 Scheduler 是怎么协作的、部署时有哪些容易翻车的地方。不管你是刚听说 vLLM 是什么还是已经在用 docker 部署大模型但遇到了性能问题应该都能从里面找到对你有用的部分。1. 推理引擎到底在解决什么问题1.1 从能出结果到出得起结果的鸿沟先把这个领域最核心的矛盾讲清楚。一个 7B 参数的模型用 transformers 库加载进来给它一句话让它续写它是能出结果的。但如果你要同时服务 100 个用户每个人都在发请求你会发现显存瞬间就爆了或者请求排队排到天荒地老。这不是模型的问题是调度和显存管理的问题。推理引擎要解决的核心矛盾可以归结为一句话GPU 显存是有限的但请求是无限且动态的。模型权重本身占一大块显存这部分是固定的。剩下的显存要用来存放每个请求的 KV Cache也就是注意力机制里缓存的键值对。传统做法是给每个请求预分配一块最大长度的显存不管它实际用不用得到。一个请求可能只生成 50 个 token但你按 2048 的长度给它预留了空间中间那些全浪费了。vLLM 提出的 PagedAttention 就是冲着这个浪费去的。它把 KV Cache 切成固定大小的块像操作系统管理内存页一样按需分配。这个思路本身不复杂但工程实现上要处理块的管理、回收、碎片整理还要保证注意力计算时能正确地把分散的块拼起来。这就是推理引擎存在的意义——它把这类系统级优化封装起来让你不用自己写 CUDA kernel 就能享受到吞吐量的提升。1.2 推理引擎和训练框架的边界在哪里很多人会混淆这两个东西的职责。训练框架比如 PyTorch、DeepSpeed、Megatron它们关心的是梯度计算、参数更新、分布式通信、混合精度训练。推理引擎不关心梯度它只做前向计算但它要在前向计算的基础上做很多训练框架不会做的事。具体来说推理引擎要处理的东西包括请求的批处理策略是等一批凑齐再算还是来一个算一个、KV Cache 的分配和回收、连续批处理continuous batching也叫 iteration-level scheduling、投机解码、量化推理、多卡并行策略张量并行、流水线并行、以及和上层服务框架的对接。这些东西训练框架要么不涉及要么实现方式完全不同。举个具体的例子。训练的时候 batch size 是固定的你凑够一个 batch 就前向反向。推理的时候请求是流式到来的如果等凑够 batch 再算先到的请求就要等很久延迟爆炸。连续批处理的做法是每个 iteration 都重新组 batch已经生成完的请求退出新来的请求加入。这个机制听起来简单但要保证 KV Cache 在请求进出时正确管理还要处理不同请求生成长度不一致的问题实现起来相当复杂。1.3 主流推理引擎的路线分化目前市面上主流的几个推理引擎各自的定位其实差别很大不是简单的谁替代谁的关系。vLLM 是目前最流行的开源方案主打高吞吐和易用性PagedAttention 和连续批处理是它的核心卖点。它的接口设计得比较友好和 OpenAI 的 API 兼容所以很多人在做 docker 部署大模型教程时首选就是它。SGLang 是后来者它在 vLLM 的基础上做了很多改进特别是 RadixAttention 对前缀共享的优化在多轮对话和结构化生成场景下表现更好。TensorRT-LLM 是 NVIDIA 官方的方案它把模型编译成 TensorRT 引擎性能上限很高但灵活性差一些模型转换和编译的流程比较重。llama.cpp 走的是另一条路它用纯 C/C 实现支持 CPU 推理和多种量化格式在端侧和资源受限环境下是首选甚至有人把它移植到了 Android 上。这几个方案的选择不是看谁性能数字高而是看你的场景。线上高并发服务选 vLLM 或 SGLang追求极致性能且有 NVIDIA 显卡选 TensorRT-LLM本地跑或者端侧部署选 llama.cpp。这个判断逻辑后面会展开讲。2. vLLM 的 EngineCore 与 Scheduler 协作机制2.1 一次请求在 vLLM 内部的完整旅程要理解 vLLM 的调度逻辑最好的方式就是跟着一个请求走一遍。当你通过 OpenAI 兼容接口发一个请求进来它首先到达的是 API Server 层这一层做参数校验、tokenize、构造请求对象。然后请求被交给 EngineCoreEngineCore 是 vLLM 的核心引擎它负责协调调度器和执行器。EngineCore 内部有一个主循环每一轮循环做几件事调用 Scheduler 决定这一轮要处理哪些请求、为这些请求分配 KV Cache 块、把调度结果交给 Executor 去实际执行模型前向计算、收集执行结果、更新请求状态。这个循环不断重复直到所有请求都生成完毕或者被中止。Scheduler 的职责是决定这一轮算谁。它维护着几个队列等待队列waiting、运行队列running、交换队列swapped。新来的请求先进等待队列。每一轮调度时Scheduler 会尝试从等待队列里取请求检查显存块是否够用够用就把它移到运行队列。运行队列里的请求如果这一轮生成了结束符或者达到最大长度就移出。如果显存不够Scheduler 可能会把一些运行中的请求换出到 CPU 内存腾出显存给更紧急的请求。Executor 是实际干活的那一层它可能是单卡的 GPUExecutor也可能是多卡的分布式执行器。它接收 Scheduler 给的调度结果组织成模型能吃的 batch跑前向计算然后把每个请求新生成的 token 返回给 EngineCore。2.2 Scheduler 的调度策略与显存块管理Scheduler 的调度逻辑是 vLLM 性能的关键。它要在一个约束下做决策这一轮所有被调度的请求它们的 KV Cache 块加起来不能超过 GPU 上可用的块数量。这个约束是硬性的超了就 OOM。调度时有一个重要的参数叫max_num_batched_tokens它限制了一轮里所有请求的 token 总数。还有一个max_num_seqs限制了一轮里最多有多少个请求。这两个参数直接影响吞吐和延迟的平衡。调大它们一轮能塞更多请求吞吐上去了但单个请求的延迟可能变高因为要等更久才能轮到。调小则相反。显存块的管理用的是块管理器BlockManager。它维护一个空闲块列表分配时从列表里取回收时放回去。这里有个细节vLLM 支持前缀共享如果两个请求的 prompt 前缀相同它们的 KV Cache 块可以共享不用重复计算。这个特性在多轮对话场景下特别有用因为每轮对话的历史部分是一样的。Scheduler 在分配块时会先做前缀匹配能共享的就共享省下来的块可以给别的请求用。还有一个容易被忽略的点是抢占preemption。当显存不够时Scheduler 必须决定牺牲谁。vLLM 的策略是优先抢占运行队列里最后加入的请求把它换出到 CPU。被换出的请求下次调度时如果显存够了再换回来。这个策略不是最优的但在大多数场景下够用。如果你发现服务在高负载下频繁抢占说明显存配置或者gpu_memory_utilization参数需要调整。2.3 Executor 如何把调度结果变成实际计算Executor 拿到 Scheduler 的调度结果后要做的事情是把逻辑上的请求列表转换成物理上的张量。这一步涉及几个关键操作把每个请求的输入 token 拼成一个大的 input_ids 张量把每个请求的 KV Cache 块地址整理成块表block table然后调用模型的前向计算。块表这个东西是 PagedAttention 的核心数据结构。它记录了每个请求的 KV Cache 分布在哪些物理块上。注意力计算时kernel 会根据块表去找到对应的块把分散的块拼成逻辑上连续的 KV。这个机制让显存分配变得灵活但也增加了 kernel 的复杂度。vLLM 自己实现了专门的 PagedAttention kernel 来处理这个。多卡场景下Executor 还要处理张量并行。模型被切分到多张卡上每张卡算一部分然后通过 all-reduce 通信汇总结果。这个通信开销在 batch 小的时候占比很高所以小 batch 场景下多卡不一定比单卡快。这也是为什么部署时要根据实际负载选择并行策略不是卡越多越好。2.4 从源码角度看 EngineCore 的主循环如果你去看 vLLM 的源码EngineCore 的主循环大致是这样的结构先调用scheduler.schedule()拿到这一轮的调度输出然后调用executor.execute_model()执行最后调用scheduler.update()更新状态。这个循环在engine_core.py里逻辑很清晰。调度输出里包含几个关键信息这一轮要处理的请求列表、每个请求要计算的 token 数、以及块表的映射关系。执行器根据这些信息组织计算。执行完之后每个请求新生成的 token 会被追加到它的输出序列里同时 Scheduler 会更新这个请求的状态比如它已经生成了多少 token、还需要多少块。这里有个值得注意的设计vLLM 把调度和执行解耦了。Scheduler 只负责决策不碰实际计算Executor 只负责执行不做决策。这种解耦让两者可以独立优化也让代码更容易维护。但代价是两者之间要传递不少数据结构有一定的开销。在高频调度场景下这个开销不能忽略。3. 不同推理引擎的选型逻辑与实测差异3.1 vLLM 和 SGLang 的真实差距在哪里SGLang 经常被拿来和 vLLM 比较很多人问 sglang 和 vllm 到底选哪个。我的实测感受是两者在基础吞吐上差距不大差异主要体现在特定场景。SGLang 的 RadixAttention 对前缀共享做了更激进的优化。它用一棵基数树来管理所有请求的前缀任何两个请求只要有公共前缀就能自动共享 KV Cache。vLLM 也有前缀共享但它的实现是基于块级别的匹配粒度粗一些。在多轮对话、few-shot 提示、或者同一个 system prompt 服务大量用户的场景下SGLang 的优势会明显一些。另一个差异是结构化生成。SGLang 内置了对正则表达式和 JSON schema 约束生成的支持做函数调用或者结构化输出时更方便。vLLM 也支持类似功能但配置起来稍微麻烦一点。但 vLLM 的生态更成熟社区更大遇到问题更容易找到答案。它的 docker 镜像维护得也更勤快各种版本的模型支持跟进得快。如果你要部署的是比较新的模型比如某些刚发布的开源模型vLLM 通常能更快支持。3.2 TensorRT-LLM 的性能上限与使用代价TensorRT-LLM 是 NVIDIA 亲儿子它把模型编译成 TensorRT 引擎在 NVIDIA 显卡上能跑到很高的性能。它的优势在于 kernel 级别的优化NVIDIA 对自己的硬件最了解能做出针对性的优化。但代价也很明显。模型要先转换成 TensorRT 的格式这个转换过程可能很慢而且对模型结构有要求。模型更新了要重新编译不像 vLLM 那样直接加载权重就行。灵活性差意味着迭代速度慢适合模型稳定、追求极致性能的生产环境。还有一个现实问题是 TensorRT-LLM 的版本兼容性。它和 CUDA、驱动、TensorRT 版本的绑定比较紧升级其中一个可能就要连带升级其他的。如果你不是专门维护推理服务的团队这个维护成本要考虑进去。3.3 llama.cpp 在端侧和 CPU 场景的不可替代性llama.cpp 的定位和前三个完全不同。它用纯 C/C 写不依赖 CUDA能在 CPU 上跑也支持 Metal、Vulkan 等后端。它的量化格式GGUF支持从 2bit 到 8bit 的各种精度模型可以压得很小。有人问 llama.cpp 有没有 Android 版答案是有的社区有人做了移植能在手机上跑小模型。虽然速度不快但能跑起来这件事本身就有意义。在边缘设备、嵌入式场景、或者没有独立显卡的机器上llama.cpp 往往是唯一可行的选择。它的缺点是吞吐量比不上 GPU 方案并发能力弱。它更适合单用户或者低并发的场景比如本地开发测试、个人助手、离线推理。3.4 选型决策表与常见误区把上面的分析整理成一个决策参考场景推荐方案核心理由线上高并发 API 服务vLLM生态成熟吞吐高OpenAI 兼容多轮对话/前缀共享密集SGLangRadixAttention 前缀共享更优追求极致性能且模型稳定TensorRT-LLMkernel 级优化性能上限高本地/端侧/CPU 推理llama.cpp不依赖 GPU量化灵活快速验证新模型vLLM新模型支持跟进快常见的误区是盲目追求性能数字。benchmark 里的吞吐量是在特定条件下测出来的你的实际负载可能完全不同。比如 benchmark 常用固定长度的 prompt 和高并发但你的实际请求可能是变长的、低并发的。选型一定要基于自己的真实场景做测试而不是看别人的数字。4. 部署实战中的坑与调优经验4.1 docker 部署 vLLM 的镜像选择与模型加载用 docker 部署 vLLM 是最常见的方式但镜像选择有讲究。官方镜像vllm/vllm-openai是带 OpenAI 兼容 API 的版本标签对应版本号。有人问 docker 镜像里带不带模型答案是不带。镜像里只有推理引擎本身模型要另外挂载或者启动时下载。启动命令的典型结构是这样的docker run --gpus all \ -v /path/to/models:/models \ -p 8000:8000 \ vllm/vllm-openai:latest \ --model /models/your-model \ --served-model-name your-model \ --gpu-memory-utilization 0.9 \ --max-model-len 4096这里有几个参数值得说。--gpu-memory-utilization控制 vLLM 能用多少比例的显存默认 0.9。这个值不是越高越好留一点余量给 CUDA context 和其他开销否则容易 OOM。--max-model-len是模型支持的最大长度设太大浪费显存设太小长请求会被截断。要根据模型本身的能力和你的实际需求来定。挂载模型目录时要注意权限问题。容器里的用户可能没有读取宿主机目录的权限导致加载失败。最简单的办法是确保模型目录对所有用户可读或者指定容器运行的用户。4.2 版本升级导致的性能回退排查vllm 新版本性能下降是社区里经常讨论的问题。我自己也遇到过升级到某个版本后吞吐量掉了不少。排查这类问题要有系统的方法。第一步是确认基线。用同一个模型、同一组请求、同样的参数在新旧版本上各跑一遍记录吞吐和延迟。不要凭感觉要有数字。第二步是看调度参数有没有变。新版本可能改了默认值比如max_num_batched_tokens的默认值调整了或者调度策略变了。对比两个版本的配置差异。第三步是看日志里的抢占情况。如果新版本频繁发生抢占说明显存管理策略变了或者有 bug。可以尝试调整gpu_memory_utilization或者max_num_seqs看是否缓解。第四步是查 issue。vLLM 的 GitHub issue 里通常已经有人报了类似问题看看有没有 workaround 或者修复版本。如果确实是回归 bug回退到上一个稳定版本是最实际的选择。4.3 特定模型部署的参数调优部署不同模型时参数需要调整。比如部署 DeepSeek 系列模型时因为模型本身比较大张量并行的配置很关键。如果单卡放不下要用--tensor-parallel-size指定卡数。但张量并行会带来通信开销卡数不是越多越好一般选能放下模型的最小卡数。部署 embedding 模型比如 qwen3-embedding-0.6b 时场景和生成模型不同。embedding 模型不需要生成长序列它的输出是一个向量。这时候max_model_len可以设小一些batch 可以设大一些因为每个请求的计算量小。vLLM 对 embedding 模型的支持是通过--task embed参数指定的。部署到特定硬件比如 L20 时要注意显存大小和计算能力的匹配。L20 的显存是 48GB放一个中等大小的模型没问题但如果是大模型就要考虑量化或者多卡。不同显卡对某些 kernel 的支持可能不同遇到报错要先确认硬件兼容性。4.4 监控与压测怎么知道服务是否健康部署完不是就完事了要知道服务在什么状态下是健康的。几个关键指标要盯着请求的 TTFTtime to first token、TPOTtime per output token、吞吐量tokens/s、以及显存使用率。TTFT 反映的是首 token 延迟它受 prompt 长度和调度排队影响。如果 TTFT 很高说明请求在排队等调度可能是并发太高或者max_num_batched_tokens设太小。TPOT 反映的是生成速度它受 batch 大小和模型计算量影响。吞吐量是整体指标它和延迟通常是矛盾的要找到平衡点。压测工具可以用 vLLM 自带的 benchmark 脚本也可以用 locust 之类的通用工具。压测时要模拟真实请求分布不要只用固定长度的 prompt。真实场景里 prompt 长度是变化的这个变化对调度影响很大。5. 推理引擎的演进方向与个人实践体会5.1 从 PagedAttention 到更细粒度的资源管理推理引擎这几年的演进主线是资源管理的粒度越来越细。PagedAttention 把 KV Cache 的管理粒度从请求级降到了块级这是一个大进步。后续的优化方向包括更细的块大小、更智能的块回收策略、以及跨请求的块共享。另一个方向是计算和调度的重叠。现在的架构里调度和执行是串行的调度完再执行执行完再调度。如果能做到调度下一轮的同时执行这一轮就能隐藏调度的开销。有些方案在尝试这个方向但实现复杂度很高。还有一个趋势是推理引擎和上层应用的融合。比如 SGLang 把结构化生成的约束直接做进了引擎里而不是让上层应用去处理。这种融合能减少数据传递的开销但也让引擎变得更复杂。5.2 我在实际部署中总结的几条经验第一条经验是不要迷信默认参数。vLLM 的默认参数是通用场景下的折中你的场景大概率不是通用场景。花时间做参数扫描找到适合自己负载的配置收益比换引擎还大。第二条经验是显存要留余量。gpu_memory_utilization设 0.95 看起来能多用显存但实际运行中 CUDA 的各种开销、碎片、临时 buffer 都可能让实际可用显存比预期少。留 10% 到 15% 的余量比较稳妥。第三条经验是监控要先行。不要等出问题了才去看日志。部署的时候就配好监控把关键指标采集起来这样出问题时才有数据可查。第四条经验是版本要谨慎升级。推理引擎迭代很快新版本可能带来性能提升也可能引入回归。升级前在测试环境验证确认没问题再上生产。生产环境最好锁定版本不要用 latest 标签。5.3 给刚接触推理引擎的人的建议如果你刚开始接触这个领域我的建议是先跑起来再深入。选一个最简单的场景用 vLLM 或者 llama.cpp 把模型跑起来感受一下推理引擎和直接调 transformers 的区别。有了直观感受之后再去理解 PagedAttention、连续批处理这些概念会容易很多。然后找一个具体的优化点深入。比如研究一下max_num_batched_tokens这个参数怎么影响性能做几组对比实验看数字变化。这种从点到面的学习方式比一上来就啃源码有效。最后是要有场景意识。推理引擎的很多设计决策是场景驱动的脱离场景去理解这些决策会很吃力。多想想你的实际场景有什么特点这些特点会怎么影响引擎的行为这样学起来才有方向感。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑