news 2026/11/3 16:31:49

大模型推理引擎选型与调优:vLLM、SGLang、TensorRT-LLM、llama.cpp 实战对比

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型推理引擎选型与调优:vLLM、SGLang、TensorRT-LLM、llama.cpp 实战对比

1. 推理引擎到底在解决什么问题

先把话说直白一点:推理引擎就是让大模型“跑起来”的那层软件。你训练完一个模型,权重文件躺在硬盘上,它自己是不会吐字的。推理引擎负责把权重加载进显存、把请求排队、把计算图调度到 GPU 上、把生成的 token 一个个拼回文本,最后通过接口返回给你。听起来像是个“胶水层”,但实际做下来,这层胶水的质量直接决定了你的吞吐量、首 token 延迟、显存占用和单位成本。

我接触推理引擎是从最朴素的transformers直接generate()开始的,那时候一张卡跑一个请求,GPU 利用率低得可怜,batch 稍微大一点就 OOM。后来陆续上手了 vLLM、SGLang、TensorRT-LLM、llama.cpp 这几套,才真正理解为什么“推理引擎”会成为一个独立赛道。它们解决的核心矛盾其实就一句话:在有限的显存和算力下,尽可能多地同时服务请求,同时把每个请求的延迟压到可接受范围。

这个矛盾拆开来看,涉及几个关键技术点。第一是KV Cache 的管理,这是显存占用的大头,传统做法按最大长度预分配,浪费极其严重,vLLM 的 PagedAttention 就是冲着这个来的。第二是请求调度,多个请求长度不一、到达时间不一,怎么组 batch、怎么抢占、怎么保证公平,这是 scheduler 的活。第三是计算优化,包括算子融合、量化、CUDA Graph 捕获、投机解码等,TensorRT-LLM 在这方面做得最激进。第四是部署形态,你是要单机单卡、多卡张量并行,还是要塞进手机里跑,llama.cpp 就是为端侧和 CPU 场景而生的。

适合读这篇内容的人,我大致分三类。一类是刚把模型跑通、准备上生产环境的工程师,你需要知道选哪个引擎、怎么配参数。一类是已经在用 vLLM 或 SGLang、但遇到性能瓶颈或调度异常的运维和开发,你需要理解内部流程才能排查。还有一类是对比选型的技术负责人,你要搞清楚这几个引擎的边界在哪,别在错误的场景上硬套。

下面我会按“整体设计思路 → 核心机制拆解 → 实操部署流程 → 问题排查”这条线展开,中间会穿插大量我在实际项目里踩过的坑和验证过的参数。涉及具体版本的地方我会标注清楚,因为推理引擎这个领域版本迭代极快,vLLM 从 0.4 到 0.6 再到 0.27 这种跨度,API 和行为都变了不少。

2. 四大推理引擎的设计思路与选型逻辑

2.1 vLLM:PagedAttention 与 EngineCore 的分层架构

vLLM 是目前社区热度最高的开源推理引擎,它的核心卖点就是PagedAttention。这个概念借用了操作系统虚拟内存分页的思路:传统 KV Cache 要求每个序列分配一块连续显存,长度按max_model_len算,一个 8K 上下文的模型,哪怕你实际只生成 100 个 token,那 8K 的显存也得先占着。PagedAttention 把 KV Cache 切成固定大小的 block(默认 16 个 token 一块),用 block table 做逻辑到物理的映射,按需分配。实测下来,同样的显存,vLLM 能同时服务的请求数通常是朴素实现的 3 到 5 倍,这个提升在长上下文场景下更明显。

vLLM 新版本的架构做了比较大的重构,核心是EngineCore这个概念。老版本里LLMEngine一把抓,调度、执行、输出处理都在一起。新版本拆成了几层:最上层是AsyncLLMEngine或LLMEngine作为对外接口,中间是EngineCore负责核心的调度循环,再往下是Scheduler和Executor。这个拆分不是纯粹为了好看,而是为了支持多进程和异步。EngineCore可以跑在独立进程里,通过 ZMQ 和前端通信,这样前端处理 HTTP 请求的进程就不会被调度循环阻塞。

具体到交互流程,我按我理解的顺序捋一遍。请求从 OpenAI 兼容的 API server 进来,经过 tokenize 之后变成一个Request对象,塞进EngineCore的输入队列。EngineCore的主循环每一步做三件事:先让Scheduler决定这一步要跑哪些请求、每个请求分配多少 block;然后把调度结果交给Executor,Executor在 GPU 上执行模型前向,产出每个请求的新 token;最后把结果收集回来,更新每个请求的状态,完成的请求移出,新 token 通过输出队列回传给前端。这个循环是持续跑的,只要队列里有活就转。

Scheduler的逻辑是 vLLM 里最值得细看的部分。它维护三个队列:waiting(等待调度的新请求)、running(正在跑的请求)、swapped(被换出到 CPU 的请求)。每一步调度时,它先看 running 队列里的请求能不能继续跑(显存够不够分配新的 block),能跑就优先跑,因为已经跑了一半的请求中断了代价大。然后从 waiting 队列里按 FCFS 取新请求,尝试分配 block。如果显存不够,它会触发preemption,把某些 running 请求换出(swap 到 CPU)或者直接重算(recompute)。这里有个关键参数max_num_seqs,控制单步最多跑多少个序列,设太小 GPU 吃不饱,设太大容易 OOM,需要根据模型大小和卡的内存调。

2.2 SGLang:RadixAttention 与结构化生成

SGLang 的定位和 vLLM 有重叠,但它的差异化在于RadixAttention和结构化生成。RadixAttention 的核心思想是用基数树(radix tree)来管理 KV Cache 的前缀共享。这个在多轮对话和 few-shot 场景下威力很大:比如你有一个固定的 system prompt,所有请求都带这段前缀,RadixAttention 会让这些请求共享同一份 KV Cache,不用每个请求都重新算一遍。vLLM 的 PagedAttention 也能做前缀共享,但 SGLang 的 radix tree 在复杂前缀匹配上更灵活,尤其是前缀有分支的情况。

SGLang 另一个杀手锏是它的前端 DSL。你可以用类似 Python 的语法描述一个生成流程,比如“先让模型输出 JSON 的 key,再根据 key 决定后面生成什么”,SGLang 的运行时能对这种结构化生成做优化,减少不必要的解码。这个在 Agent 和工具调用场景下特别有用,因为那些场景的生成往往有很强的结构约束。

SGLang 和 vLLM 怎么选?我的经验是:如果你的场景是标准的“输入 prompt 输出文本”,两者差距不大,vLLM 生态更成熟、文档更全。如果你的场景有大量前缀共享(多轮对话、固定 system prompt)或者需要结构化约束生成,SGLang 往往能快出一截。另外 SGLang 在某些模型上的 kernel 实现更激进,比如它对 DeepSeek 系列的支持在早期比 vLLM 更早适配。

2.3 TensorRT-LLM:极致性能与编译代价

TensorRT-LLM 是 NVIDIA 官方出的,走的是“编译优化”路线。它会把你的模型编译成一个 TensorRT engine,这个过程会做大量的图优化、算子融合、kernel 自动调优。编译一次可能要几十分钟甚至更久,但编译出来的 engine 在 NVIDIA 卡上的性能通常是三者里最好的,尤其是在 FP8、INT4 量化场景下。

代价是灵活性差。模型结构变了要重新编译,换卡要重新编译,甚至换 TensorRT 版本都可能要重编。而且它对模型的支持不如 vLLM 广,新模型出来往往要等官方适配。所以 TensorRT-LLM 适合那种模型固定、追求极致吞吐、有专门团队维护的场景,比如大厂的在线服务。个人开发者和小团队用 vLLM 或 SGLang 的性价比更高。

2.4 llama.cpp:端侧与 CPU 场景的王者

llama.cpp 和前三个不是一个路数。它用纯 C/C++ 实现,支持 CPU、CUDA、Metal、Vulkan 等多种后端,核心优势是量化和低资源占用。GGUF 格式就是它推起来的,4-bit 量化后一个 7B 模型只要 4GB 左右内存,能在普通笔记本甚至手机上跑。Android 版 llama.cpp 已经比较成熟,配合 Termux 或者专门的 App 可以本地跑小模型。

llama.cpp 的吞吐没法跟 vLLM 比,它本来也不是为高并发设计的。但它的价值在于“哪里都能跑”。你做端侧应用、离线推理、隐私敏感场景,llama.cpp 是首选。它的llama-server也提供了 OpenAI 兼容接口,小规模自用完全够。

引擎核心优势适用场景主要短板
vLLMPagedAttention、生态成熟、易部署在线服务、通用场景极致性能略逊 TRT-LLM
SGLangRadixAttention、结构化生成多轮对话、Agent、约束生成生态和文档相对少
TensorRT-LLM极致性能、量化支持好模型固定的大规模服务编译慢、灵活性差
llama.cpp端侧、CPU、量化本地、离线、移动端并发能力弱

3. 核心机制拆解:调度、显存与执行

3.1 Scheduler 的抢占逻辑与参数调优

vLLM 的 scheduler 抢占逻辑是我调试时花时间最多的地方。它的基本规则是:running 队列里的请求优先,显存不够时触发抢占。抢占有两种模式,swap和recompute。swap是把被抢占请求的 KV Cache 从 GPU 拷到 CPU 内存,等显存够了再拷回来;recompute是直接丢掉 KV Cache,等重新调度时把之前的 token 重新算一遍。swap省算力但费 PCIe 带宽,recompute省显存但费算力。默认策略会根据情况选,你可以通过--preemption-mode强制指定。

这里有个很隐蔽的坑:如果你的请求长度差异极大,比如有的请求只要生成 10 个 token,有的要生成 4000 个,长请求很容易被反复抢占,导致它的实际延迟爆炸。我遇到过一次,一个长文档摘要请求在混合负载下跑了 3 分钟还没完,查日志发现被抢占了十几次。解决办法是调大gpu_memory_utilization(默认 0.9,可以试 0.92 到 0.95,但别太满,要留空间给激活值),或者用--max-num-seqs限制并发数,给长请求留出空间。更精细的做法是用--scheduling-policy切换策略,不过 vLLM 目前主要就是 FCFS,公平性调度还在演进。

max_num_batched_tokens这个参数也值得说。它控制单步最多处理多少个 token(包括 prefill 和 decode)。设大一点,prefill 阶段吞吐高,但 decode 阶段的延迟可能变大;设小一点,延迟低但吞吐下降。默认值通常是 2048 或 4096,我一般会根据 SLA 调:延迟敏感就调小到 1024 到 2048,吞吐优先就调到 8192 甚至更高。这个参数和max_num_seqs要配合调,两个都大容易 OOM。

3.2 KV Cache 显存计算与 block 管理

要调好 vLLM,你得会算 KV Cache 的显存。公式不复杂:每个 token 的 KV Cache 大小 =2 × num_layers × num_kv_heads × head_dim × dtype_bytes。注意是num_kv_heads不是num_attention_heads,因为 GQA(分组查询注意力)模型里 KV head 数比 Q head 少很多。以 Qwen3-0.6B 为例,假设 28 层、8 个 KV head、head_dim 128、FP16(2 字节),那每个 token 的 KV 是2 × 28 × 8 × 128 × 2 = 114688字节,约 112KB。一个 8K 上下文的请求就要 896MB,这还只是一个请求。

vLLM 启动时会先加载模型权重,然后根据gpu_memory_utilization算出可用于 KV Cache 的显存,再除以每个 block 的大小(block_size × 每 token KV)得到总 block 数。你可以在启动日志里看到# GPU blocks: XXXX这行,这个数字直接决定了你能同时跑多少请求。如果这个数字很小,比如只有几百,那说明显存被权重占太多了,要么换更小的模型,要么用量化,要么加卡。

block_size默认 16,可以设成 8、32、64。设小了 block table 开销大,设大了内部碎片多。一般 16 是平衡点,除非你有特殊需求,否则别动。有个细节:block_size 影响 PagedAttention kernel 的效率,某些卡上 32 可能比 16 快,这个要实测。

3.3 Executor 与多卡并行

Executor是真正干活的层。单卡就是GPUExecutor,多卡有RayGPUExecutor和MultiprocessingGPUExecutor。多卡并行走的是张量并行(TP)或流水线并行(PP)。TP 是把每一层的权重切到多张卡上,每层计算时卡间要 all-reduce 通信;PP 是把不同层放到不同卡上,像流水线一样传。TP 通信量大但延迟低,PP 通信量小但有流水线气泡。一般单机多卡用 TP,跨机用 PP 或者 TP+PP 混合。

TP 的度数要和注意力头数匹配。比如模型有 32 个 attention head,你 TP=4,那每张卡分 8 个 head,没问题。但如果模型只有 8 个 head,你 TP=16,那就分不了了,vLLM 会报错。所以选 TP 度数前先看模型的 head 数。另外 TP 会带来通信开销,TP=2 通常有加速,TP=4 可能加速比就不到 2 了,TP=8 有时候反而更慢,因为通信成了瓶颈。这个要实测,别盲目加卡。

4. 实操部署:从 Docker 到生产环境

4.1 Docker 镜像选择与模型加载

vLLM 官方提供了vllm/vllm-openai镜像,这是最省事的部署方式。但这里有个新手最容易困惑的问题:镜像里带模型吗?答案是默认不带。镜像里只有 vLLM 的代码和依赖,模型要你自己挂载或者让它运行时下载。所以启动命令里通常要挂载一个模型目录,或者设置HF_HOME让它从 HuggingFace 拉。

以部署 Qwen3-Embedding-0.6B 为例,假设你用vllm/vllm-openai:v0.27.1这个镜像(版本号按你实际用的填),命令大概是这样:

docker run --gpus all \ -v /data/models:/models \ -p 8000:8000 \ --shm-size 16g \ vllm/vllm-openai:v0.27.1 \ --model /models/Qwen3-Embedding-0.6B \ --served-model-name qwen3-embedding \ --task embed \ --max-model-len 8192 \ --gpu-memory-utilization 0.9

几个点要注意。--shm-size一定要给够,vLLM 多进程通信用共享内存,默认 64MB 会直接崩,我一般给 16g。--task embed是 embedding 模型专用的,普通生成模型不用加。--served-model-name决定 API 里model字段填什么,不设的话默认是模型路径,很长很难用。

如果是部署 DeepSeek 这种大模型,模型文件可能上百 GB,从 HuggingFace 拉很慢,建议提前用huggingface-cli download下到本地再挂载。另外 DeepSeek 系列有些是 MoE 结构,vLLM 对 MoE 的支持在较新版本才完善,老版本可能跑不起来或者性能很差,选镜像时要注意版本。

4.2 关键启动参数与调优

启动参数里,除了前面说的gpu_memory_utilization、max_num_seqs、max_num_batched_tokens,还有几个必调的。

--dtype控制权重和计算的精度。默认auto,会根据模型配置选。如果你的卡支持 BF16,模型也是 BF16,那就用 BF16。如果显存紧张,可以用--quantization做量化,vLLM 支持 AWQ、GPTQ、FP8 等。FP8 在 H100 这类卡上效果好,老卡不支持。

--tensor-parallel-size就是 TP 度数,前面说过要和 head 数匹配。

--enable-prefix-caching开启前缀缓存,多轮对话场景必开,能显著降低重复前缀的计算。但注意它和某些量化方式可能冲突,开了之后如果报错就先关掉排查。

--max-model-len是最大上下文长度。这个值直接影响 KV Cache 的 block 分配上限,设太大浪费显存,设太小长请求会被截断。要按你的实际业务设,别直接抄模型的max_position_embeddings。

--swap-space是 CPU swap 空间大小,默认 4GB。如果你的请求长度差异大、抢占频繁,可以调大到 16GB 或 32GB,但前提是机器内存够。

4.3 客户端接入与 Chatbox 配置

服务起来之后,验证最简单的方式是 curl:

curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen3-embedding", "messages": [{"role": "user", "content": "你好"}] }'

如果用 Chatbox 这类客户端,配置里选 OpenAI 兼容,Base URL 填http://你的IP:8000/v1,API Key 随便填一个非空字符串(vLLM 默认不校验),模型名填--served-model-name设的那个。连不上先检查端口有没有映射对、防火墙有没有放行、--host是不是0.0.0.0(默认就是)。

4.4 Windows 与 Android 场景

vLLM 官方不支持 Windows 原生,Windows 上要么用 WSL2,要么用 Docker Desktop。WSL2 里跑 vLLM 是可以的,但要注意 WSL 的内存限制,默认可能只给一半物理内存,要在.wslconfig里调。Docker Desktop 走 WSL2 后端,GPU 直通需要装好驱动和 CUDA on WSL。

llama.cpp 的 Android 版是另一条路。你需要交叉编译或者用预编译的二进制,把 GGUF 模型推到手机存储,然后用llama-server起服务。手机内存有限,7B 的 Q4 量化模型大概占 4GB,中高端机跑得动,但速度也就每秒几个 token。适合做离线小助手,别指望高并发。

5. 常见问题与排查实录

5.1 启动就 OOM 怎么办

这是最高频的问题。排查顺序:先看模型权重占了多少,再看 KV Cache 能分多少。启动日志里会打印模型加载后的显存占用和可用 block 数。如果 block 数是个位数或者零,说明权重就快把显存占满了。解决办法按优先级:换更小的模型、用量化版本(AWQ/GPTQ)、降低gpu_memory_utilization反而可能更糟(因为留给 KV 的更少),正确做法是降max_model_len和max_num_seqs,或者加卡。

有个反直觉的点:gpu_memory_utilization调高不一定好。它控制 vLLM 预留给 KV Cache 的比例,但激活值、CUDA Graph 捕获也要显存。设成 0.95 有时候会在跑起来之后 OOM,因为运行时还有额外开销。我一般从 0.85 开始试,稳定了再往上加。

5.2 新版本性能下降的排查

vLLM 版本迭代快,偶尔会出现“升级后反而变慢”的情况。我遇到过一次,从某个版本升上去之后吞吐掉了 30%。排查思路:先确认是不是默认参数变了,新版可能改了max_num_batched_tokens的默认值或者调度策略。然后看是不是 CUDA Graph 没生效,日志里搜capture相关。再就是看是不是新版本对某个 kernel 的实现变了,这个只能对比 benchmark。

应对策略是:生产环境别追最新版,用经过验证的稳定版。升级前先在测试环境跑 benchmark,对比吞吐和延迟。如果确实新版有问题,回退到旧版,等社区修。GLM、MiniMax 这类模型用哪个 vLLM 版本,最好查官方文档或者社区 issue,别自己瞎试。

5.3 请求卡住或延迟异常

请求卡住通常是调度问题。先看日志里有没有反复的 preemption 记录,如果有,说明显存紧张导致请求被反复换出。调大gpu_memory_utilization或者降并发。如果日志里没有 preemption 但请求就是不动,可能是max_num_seqs设太小,请求在 waiting 队列里排不上。还有一种可能是某个请求的 prompt 特别长,prefill 阶段占满了max_num_batched_tokens,导致其他请求的 decode 被饿死。这种情况要开 chunked prefill(--enable-chunked-prefill),把长 prefill 切成小块,和 decode 交替跑。

5.4 常见问题速查表

现象可能原因排查动作解决方向
启动 OOM权重+KV 超显存看 block 数日志量化/降 max_model_len/加卡
吞吐低batch 太小看 GPU 利用率调大 max_num_seqs
延迟高抢占频繁搜 preemption 日志调大显存利用率/降并发
请求卡住长 prefill 饿死 decode看请求长度分布开 chunked prefill
升级变慢默认参数或 kernel 变化对比 benchmark回退版本
多卡不加速TP 通信瓶颈看卡间通信降 TP 度数或换 PP

5.5 几个我踩过的坑

第一个坑是--shm-size。我第一次用 Docker 部署,没设这个,服务起来跑几个请求就崩,报共享内存相关的错。查了半天才反应过来是 Docker 默认 shm 太小。这个坑很隐蔽,因为单请求测试可能不崩,一上并发就挂。

第二个坑是模型路径权限。挂载的模型目录如果权限不对,容器里读不了,报错信息还不直观。建议挂载前chmod -R 755一下。

第三个坑是 embedding 模型和生成模型混用。vLLM 一个实例只能跑一种任务类型,你不能在同一个服务里既做 embedding 又做生成。要分开起两个实例,端口错开。

第四个坑是max_model_len设太大。我一开始图省事直接设成模型的最大值,结果 block 数少得可怜,并发上不去。后来按实际业务最长请求设,并发直接翻倍。这个参数不是越大越好,是按需设。

6. 一些实战体会

推理引擎这个领域,文档和实际行为之间总有 gap。我现在的习惯是,任何参数调整都先在小流量上验证,看日志里的 block 数、preemption 次数、GPU 利用率这几个指标,再决定要不要推到生产。benchmark 工具推荐用 vLLM 自带的benchmarks/benchmark_serving.py,能模拟真实请求分布,比单纯压测有意义。

选型上,我的默认建议是:通用场景 vLLM 起步,遇到前缀共享或结构化生成再考虑 SGLang,追求极致性能且有维护能力上 TensorRT-LLM,端侧和离线用 llama.cpp。别一上来就追求“最强”,先把业务跑通,再根据瓶颈优化。大部分场景下,把max_num_seqs、max_num_batched_tokens、gpu_memory_utilization这三个参数调明白,性能就能提升一大截,比换引擎的收益还大。

最后分享一个小技巧:vLLM 的日志级别用--disable-log-requests可以关掉每个请求的详细日志,高并发下这个日志本身就很耗性能。但排查问题时记得打开,不然你看不到 preemption 和调度细节。这个开关我一般是在稳定运行后关掉,出问题再开。

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

EasyPOI合并单元格:mergeVertical与needMerge实战

1. 合并单元格在 EasyPOI 里到底合的是什么 做 Java 导出这行的人,多半都经历过这种场面:Excel 文件是导出来了,字段没错、数据也对,业务方打开看了三秒钟,回一句"这个部门名怎么每行都在重复,能不能合…

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

ROS中spin、spinOnce与rospy.spin:回调队列与线程模型

ROS 节点里最容易被当成"样板代码"抄下去的三个东西,大概就是ros::spin()、ros::spinOnce()和rospy.spin()了。教程里往往一行带过,一句"加上它就能收到消息"就结束。可真到了项目里,这三行决定的是整个节点的事件处理模…

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

PCB数据驱动布线:从规则约束到物理感知的AI实践

1. 这不是“AI替代工程师”,而是布线逻辑的底层迁移“规则已死!AI 布线终局是数据驱动”——这句话刚在EDA圈子里冒头时,我正蹲在嘉立创EDA里手动调一根0.2mm差分对的等长误差。旁边同事甩来一句:“你还在拉蛇形线?隔壁…

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

LT6911UXC:HDMI2.0转MIPI DSI的4K@60Hz协议桥接方案

简介:本资源为凌通科技(Lontium)LT6911UXC芯片的官方英文版规格书(Datasheet)R1.2版本,面向嵌入式硬件工程师、显示接口开发人员及VR/智能终端系统设计者,解决HDMI2.0到MIPI DSI/CSI/LVDS多协议…

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

char数组转字符串:内存、编码与长度语义的工程避坑指南

调试一个串口采集程序时,日志里打印出来的数据永远只有第一个字节,后面全是空白;换到另一个模块,收到的字符串末尾总是多出几个"烫烫烫";再换一个场景,明明数组里存的是"你好"两个汉字…

作者头像 李华