news 2026/11/3 16:31:52

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
推理引擎选型与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、构造请求对象。然后请求被交给 EngineCore,EngineCore 是 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 监控与压测:怎么知道服务是否健康

部署完不是就完事了,要知道服务在什么状态下是健康的。几个关键指标要盯着:请求的 TTFT(time to first token)、TPOT(time 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这个参数怎么影响性能,做几组对比实验,看数字变化。这种从点到面的学习方式比一上来就啃源码有效。

最后是要有场景意识。推理引擎的很多设计决策是场景驱动的,脱离场景去理解这些决策会很吃力。多想想你的实际场景有什么特点,这些特点会怎么影响引擎的行为,这样学起来才有方向感。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 16:31:51

Kafka监控体系构建:从Prometheus到Grafana实战

1. Kafka监控体系构建的必要性在分布式消息系统中,Kafka集群的健康状况直接影响着整个数据管道的可靠性。我经历过多次线上事故,都是由于监控不到位导致小问题演变成服务中断。比如某次磁盘I/O瓶颈未被及时发现,最终造成生产者大面积阻塞。这…

作者头像 李华
网站建设 2026/11/3 16:31:00

本体驱动RAG:从向量检索到语义规划,破解企业级知识库召回瓶颈

1. 为什么传统RAG走到一半就“卡脖子”了做过RAG项目的人大概都有过这种体验:Demo阶段效果惊艳,一旦上线面对真实用户的提问,召回率就断崖式下跌。用户问“去年Q3华东区哪些经销商同时满足返点超5%且退货率低于2%”,传统向量检索要…

作者头像 李华
网站建设 2026/11/3 16:29:12

本地AI学习工具:FastAPI+Gradio+Ollama开箱即用方案

1. 项目概述:一个真正“开箱即用”的本地AI学习软件,不是玩具,是工具 我做了一个本地 AI 学习软件,免费开源本地运行——这句话不是宣传话术,而是我过去三个月每天调试、重构、压测后的真实交付物。它不依赖任何云服务…

作者头像 李华
网站建设 2026/11/3 16:28:34

视觉引导旋转轴精度不准?数据流排查与手眼标定实操指南

生产线上机械臂对着来料抓取,视觉系统识别位置后引导旋转轴转到指定角度,结果发现每过几个节拍就偏那么零点几度。这个"机器人视觉引导旋转轴不准"的问题,我相信做集成的朋友都遇到过。视觉、机器人、PLC、旋转轴,每个单…

作者头像 李华
网站建设 2026/11/3 16:28:29

Java-SSM378音乐网站付费管理系统开发实践

1. 项目概述与核心价值Java-SSM378音乐网站付费管理系统是一个典型的数字内容商业化解决方案,采用前后端分离架构设计。我在实际开发中发现,这类系统最核心的挑战在于平衡版权保护与用户体验。系统以SpringBoot作为后端基础框架,配合Vue3构建…

作者头像 李华
网站建设 2026/11/3 16:27:39

ESP32与AIoT设备端接入云端的最佳实践

1. ESP32与AI设备端接入的背景解析 去年在帮客户部署智能家居中控系统时,我遇到了一个典型需求:如何让搭载TensorFlow Lite Micro的ESP32设备将识别结果可靠地上报至云端管理系统。这个看似简单的"设备-云端"通信链路,在实际部署中…

作者头像 李华