vLLM 技术内幕——把显存当操作系统管

基于 vLLM 主线(commit 8151f2ad,2026-08-12)与稳定版 v0.27.1(2026-08-11)。源码位置都钉到该 commit,论文是 Kwon 等人的 PagedAttention(SOSP 2023,arXiv:2309.06180)。

一句话先行:大模型服务的钱,一大半烧在显存上。vLLM 把操作系统管内存的老办法——分页、页表、引用计数、写时复制——搬进了 KV 缓存,浪费从七成压到 4% 以内,吞吐最高翻 24 倍。下面拆开看它是怎么做到的。


1. 穷在哪:六七成显存白白扔掉

跑大模型推理,贵的不只是算力,更是显存。一个 13B 模型,每个 token 的 KV 缓存约 800 KB(论文 §3);2048 token 的一段话,光 KV 就吃 1.6 GB。上百个请求一并发,KV 总量经常比模型本身还大。

老引擎(FasterTransformer、Orca、直接拿 HuggingFace Transformers 跑)有个笨办法:每个请求的 KV 预先划一整块连续显存,长度按"最长能到多少"算。好比请客先按 100 人订桌,结果只来 30 人,70 张桌子空着。论文拿真实负载(ShareGPT)一测:KV 里真正有货的部分只有 20.4%–38.2%,六七成显存纯浪费(碎片 + 预留空位)。

vLLM 是 UC Berkeley 搞出来的,招式就一招:学操作系统管内存。从此"显存浪费"被摁到 4% 以下,吞吐相对 HF Transformers 最高 24 倍,相对同期最强的 FasterTransformer、Orca 也有 2–4 倍。

今天的 vLLM 早不只是一个显存技巧,而是一整套推理服务引擎:OpenAI 兼容 API、多种并行、量化、前缀缓存、投机解码、结构化输出、LoRA,单卡到跨节点都能铺。


2. PagedAttention:KV 缓存的"格子间革命"

2.1 切块 + 索引,跟操作系统一模一样

做法像把大仓库改成格子间:每个序列的 KV 按 16 个 token 一块切开(论文 §7.2 试过,这是利用率和碎片的甜点)。物理显存里块和块不挨着没关系,靠一张**块表(block table)**记"第几块逻辑 → 住哪间物理"——跟操作系统的页表(虚拟页 → 物理页)一个模子:块就是页,token 就是字节,序列就是进程。

算注意力时也不拼大矩阵了,逐块读:拿当前 query 跟每间房里的 key 算点积,再把 value 按块累加。块表保证各家只看自家货,不串门。

2026 年 V1 代码里这套架子还在:

  • BlockPool 给整块 GPU KV 显存预建一排"房间卡" KVCacheBlock(vllm/v1/core/block_pool.py:174)。
  • 每张卡记着 block_id、ref_cnt(几家合租)、满间时的前缀哈希、空房链表指针(vllm/v1/core/kv_cache_utils.py:118)。
  • 每个请求的块表就是 req_to_blocks: dict[request_id, list[KVCacheBlock]],按逻辑顺序排队,每项指一间物理房(vllm/v1/core/single_type_kv_cache_manager.py:357)。
  • 写货时 PagedAttention.write_to_paged_cache 调融合算子 reshape_and_cache 把新 KV 塞进对应房间(vllm/v1/attention/ops/paged_attn.py:31);KV 张量按块排成 [2, num_blocks, num_kv_heads, head_size/16, block_size, 16](paged_attn.py:17)。

fork 并行采样 / beam

逻辑块序列

有序排列

块表映射 (page table)

共享 (ref_cnt > 1)

满块时登记哈希

持有全部物理块

或空闲(LRU) 或已分配

REQUEST

SEQUENCE

LOGICAL_BLOCK

PHYSICAL_BLOCK

PREFIX_HASH

BLOCK_POOL

FREE_QUEUE

2.2 三个实打实的好处

显存几乎不浪费。 碎片只剩每个序列最后那间没住满的房(不到 16 token 的量),实测浪费不足 4%。同样显存能接的并发,相对 Orca(Oracle) 多 2.2 倍、相对 Orca(Max) 多 4.3 倍(论文 §6.2)。

吞吐翻几倍。 同等延迟下相对 FasterTransformer 和 Orca 2–4 倍(论文摘要);ShareGPT 上相对 FasterTransformer 最高 22 倍;相对 HF Transformers 最高 24 倍,相对 TGI 最高 3.5 倍。

货能合租。 每间房带 ref_cnt。并行采样时,所有样本的 prompt 指着同一批房;真分叉了才对最后一间合租房写时复制:谁要写一间 ref_cnt>1 的房,就另开新房、复印、老房计数减一(_apply_cow,single_type_kv_cache_manager.py:403)。实测:并行采样省 6.1%–9.8% 显存,beam search 省 37.6%–55.2%(ShareGPT 上最高 66.3%)。

代价也得说:论文 §7.1 测过,PagedAttention 内核比 FasterTransformer 的连续内核慢约 20%–26%——但端到端还是碾压,省下的显存换来的并发早就把这点内核开销赚回来了。


3. 调度:公交车式上下客 + 长面包切片

3.1 连续批处理:到站就上下,不等整车

vLLM 沿用 Orca 的迭代级调度:每次前向一结束,干完的下车,新来的上车。PagedAttention 让"加人减人"不用重新包整车,所以每两次前向之间都能变阵,几乎零成本。

V1 引擎主循环就是一遍遍 step()(vllm/v1/engine/core.py:581):

scheduler_output   = scheduler.schedule(...)      # 这步拉谁上车
future             = executor.execute_model(...)  # 非阻塞跑一轮
model_output       = future.result()
engine_core_output = scheduler.update_from_output(...)  # 结算、下车、还房卡

干完的请求在 update_from_output 里触发 finish_requests,kv_cache_manager.free() 把房间"从尾往头"还给池子(kv_cache_manager.py:568),下一轮 schedule() 马上把排队的人补进来。上下车都在两次前向之间,不等整条线路跑完——"连续"说的就是这个。

一轮 step() 全程长这样:

否

是

否

是

是

否

EngineCore.run_busy_loop

_process_input_queue
新请求进 waiting

scheduler.schedule()

token_budget
用尽?

取 running 请求
allocate_slots()

Executor.execute_model()
非阻塞前向

物理块够?

抢占: num_computed_tokens=0
free 全部块 → 回 waiting

sample_tokens()

scheduler.update_from_output()

有请求完成?

kv_cache_manager.free()
块归还池

3.2 不分 prefill 和 decode:长面包切着吃

V0 时代还分"备菜阶段"(prefill)和"上菜阶段"(decode),一个长 prompt 的 prefill 一次前向独占整步,别人全罚站。V1 把这堵墙拆了:prompt 和生成的 token 一视同仁,调度决策就是一张小纸条 {request_id: num_tokens}——这步给你几个 token 名额(V1 博客 §2)。

纸条背后是两个饭量上限(scheduler.py:439 附近):

  • token_budget = max_num_scheduled_tokens(默认 = max_num_batched_tokens = 2048);
  • input_budget = max_num_batched_tokens,卡这步拼起来的总 token 数。

每个请求这步能吃几个 token,看三者谁最小:剩的量 / token_budget / input_budget(scheduler.py:882)。prompt 超了预算?切块,剩下的下几步接着吃,这步里 decode 照常跑。这就是分块预填充,V1 默认开、不用配(docs/usage/v1_guide)。还能用 long_prefill_token_threshold 给单个 prefill 块再设上限。

V0 和 V1 的本质差别:V0 只能跑"纯备菜"或"纯上菜"的专车;V1 一车混装,备菜上菜一起拉。

光讲道理容易晕,看个例子:3000 token 的长 prompt 进来,A、B 两个请求正在 decode,每步预算 2048:

Step 3 起

Prefill 完成
转入纯 Decode

Decode A

Decode B

Step 2 (预算 2048)

Prefill 块2
token 2048–3000

Decode A

Decode B

Step 1 (预算 2048)

Prefill 块1
token 0–2047

Decode A

Decode B

长 prompt 切两刀,块1 占满 Step 1,块2 在 Step 2 收尾;A、B 的 decode 步步不停,没被长 prompt 堵死。首 token 不用再"等整条长 prompt 一次算完",decode 吞吐几乎无损——这就是切片的好处。


4. 前缀缓存:公共开头只算一遍

很多请求开头都一样:同一段 system prompt、同一组 few-shot 例子。算一遍存下来,后面复用,就是前缀缓存。

哈希是"连锁印章":每块的哈希 = 上一块哈希 + 本块 token ID(+ LoRA/多模态附加键)。一个印章就代表"到这块为止的整条前缀"(kv_cache_utils.py:577)。

找货:find_longest_cache_hit 从第 0 块顺着印章往后盖,第一个盖不上的停(后面必然全 miss),去 cached_block_hash_to_block 表查(single_type_kv_cache_manager.py:729)。命中的块 touch()——引用加一,原本在待淘汰队的就摘出来(block_pool.py:702)。

存货:前向之后 cache_blocks 把新住满的块登记印章、入表(single_type_kv_cache_manager.py:425)。只存验过货的 token,投机解码的草稿不存(kv_cache_manager.py:555)。

扔货:房间释放时引用减一;还带着印章、计数归零的进队尾(最后扔,留着当缓存),没印章的进队头(马上复用)。队是 O(1) 双向链表,中间删除不心疼——论文说的"常数时间淘汰"就是它。

V0 的前缀缓存 Python 开销大,默认关;V1 重做后 0% 命中时吞吐损失 <1%,于是 V1 默认开(config/cache.py:96)。实测:LLaMA-13B 上 341 token 的 few-shot 前缀,相对 Orca(Oracle) 吞吐 3.58 倍。

命中公共开头时,请求走的是这条近道——命中段直接住现房、跳过计算,只算没命中的尾巴:

是

否

新请求到达

hash 第 0 块

cached_block_hash
_to_block 命中?

touch(): ref_cnt+1
复用物理块

分配新块
前向计算 KV

沿链式哈希
算下一块哈希

命中段跳过计算
仅算未命中后缀

哈希->物理块

按块切分

链式哈希 (含父哈希)

ref_cnt=0 进空闲队列

LRU 双向链表

命中则 touch 提升留存

PREFIX_HASH

PHYSICAL_BLOCK

REQUEST

TOKEN_CHUNK

FREE_QUEUE


5. 房卡不够了:抢座只重算,不搬家

KV 缓存管理器(vllm/v1/core/kv_cache_manager.py:118)就是"房卡管理员"。每步 allocate_slots() 先算要几间新房、池里够不够;不够就抢座。_preempt_request(scheduler.py:1284)下手干脆:被抢的人 num_computed_tokens 清零、房卡全收、回排队——纯重算。

这里有个跟论文、跟 V0 都不一样的事实:

  • 论文 §4.5 当年做了两种抢座恢复:Swap(把房搬到 CPU)和 Recompute(扔了重算)。§7.3 实测:重算开销不随块大小变,Swap 在小块时贵得飞起;重算延迟从不超过 Swap 的 20%,块 16–64 时两者差不多。当年默认就是重算。
  • V1 直接砍了 Swap,只留重算(docs/usage/v1_guide 明确写 “GPU<>CPU KV Cache Swapping: Removed”)。道理还是那个:在浪费已干掉的前提下,再养一套搬家公司又复杂又不值。被抢的人回排队后,之前的前缀可能还缓存在池里,还能少算一段。

块够

块不够

请求完成

Scheduler.schedule

allocate_slots()

分配物理块 / 更新块表

抢占最低优先级请求

num_computed_tokens = 0
free 全部块

退回 waiting 队列

execute_model

update_from_output

kv_cache_manager.free()

块归还 BlockPool


6. 全链路:从 HTTP 到 GPU

6.1 四种进程,各干各的

V1 用多进程拆活儿,吞吐最高、CPU 开销最低(出自 docs/design/arch_overview.md):

  • API Server:门口迎宾,收 HTTP(OpenAI 兼容)、分词、多模态装货,结果流式送回。用 ZMQ 跟 EngineCore 说话。默认 1 个,DP>1 时跟 DP 数对齐。
  • EngineCore:后厨总管,跑调度、管房卡、指挥 GPU 工人。每个 DP rank 一个,死循环不停派活。
  • GPU Worker:每卡一个,背权重、跑前向、管本卡显存。一个 EngineCore 下工人总数 = DP × PP × TP。
  • DP Coordinator:DP>1 才有,跨 rank 匀活儿。

6.2 一个请求的一生

OpenAI 兼容请求

InputProcessor 分词

input socket

SchedulerOutput

每卡一个 WorkerProc

ModelRunnerOutput

output socket

OutputProcessor 解词

SSE 流式

HTTP 客户端

API Server 进程

EngineCoreRequest

AsyncMPClient (ZMQ)

EngineCore 进程

Scheduler

KV Cache Manager

Executor

GPU Worker

GPUModelRunner

PagedAttention 内核

代码钉在哪:

  • 入口 api_server.py:110 的 build_async_engine_client() → AsyncLLM.from_vllm_config()(:168)。
  • AsyncLLM(vllm/v1/engine/async_llm.py:72)只接三样:InputProcessor(请求→EngineCoreRequest)、OutputProcessor(EngineCoreOutputs→RequestOutput)、EngineCoreClient(→ZMQ 客户端)。
  • AsyncMPClient(core_client.py:977):input 走 ROUTER、output 走 PULL(:554)。
  • EngineCore(core.py:104)外面包着 EngineCoreProc(:1005);run_engine_core(:1269)是后台入口,run_busy_loop(:1373)是那个转不停的圈。
  • 调度结果经 Executor 发到 Worker.execute_model(gpu_worker.py:1026),再到 GPUModelRunner.execute_model(gpu_model_runner.py:4277):更新状态 → 备料 → 建注意力元数据 → 前向 → 算 logits → 采样(sample_tokens,:4656)。
Worker/GPUSchedulerEngineCoreAsyncMPClientAPI Server客户端Worker/GPUSchedulerEngineCoreAsyncMPClientAPI Server客户端loop[每个 step]POST /v1/chat/completionsInputProcessor 分词 → EngineCoreRequestZMQ inputadd_request (进 waiting)schedule() 选 running + waitingallocate_slots()SchedulerOutputexecute_model()ModelRunnerOutputupdate_from_output()EngineCoreOutputs (ZMQ output)OutputProcessor 解词SSE 流式 token

6.3 并行:四层叠罗汉

Executor 在 vllm/v1/executor/abstract.py:48 的 get_class() 里三选一:

  • uni → UniProcExecutor:工人跟总管住一屋,调试用;
  • mp(默认)→ MultiprocExecutor:每卡一个 WorkerProc;
  • ray → RayExecutorV2 / RayDistributedExecutor:跨节点大场面。

并行四个维度随便叠:

  • TP(张量并行):一层切几瓣,单机多卡最常用;
  • PP(流水线并行):按层切段,max_concurrent_batches 拿 batch 填气泡;
  • DP(数据并行):几套完整副本,DP Coordinator 匀活儿;
  • EP(专家并行):MoE 的专家分卡住,v0.9.0 起接 NIXL,后面还有 elastic_ep 动态伸缩。

7. 家底(截至 v0.27.1):模型、量化、硬件、服务

7.1 模型:240 个原生架构

主线登记 240 个原生模型架构:123 纯文本 + 117 多模态(含语音识别/转录、实时语音)。另有池化模型:embedding 39、分类 5、token 分类 7、打分/重排 12、奖励模型 7。

主流家族基本全有:Llama 2/3/4、Mistral/Mixtral/Ministral、Qwen2/2.5/3/3.5 和 VL/Omni 系、DeepSeek-V2/V3/V3.2/V4 与 R1、Gemma 2/3/3n/4、GPT-OSS、Phi、GLM-4.x/5.x、Kimi(K2/K3/VL)、MiniMax(M2/M3/VL)、Nemotron、Granite、InternLM、Jamba、Mamba2、Falcon、OLMo;视觉有 InternVL 2/2.5/3、LLaVA 系、Pixtral、Molmo;语音有 Whisper、Qwen3-ASR、Kimi-Audio、GLM-ASR、FireRedASR2;视频有 LLaVA-NeXT-Video、InternVideo。

榜上无名怎么办?Transformers 建模后端兜底(encoder-only/decoder-only/MoE、图文音多模态、trust_remote_code 自定义模型),插件系统补 encoder-decoder(BART、Florence-2 等)。注意红线:encoder-decoder 核心已不再原生支持,必须走插件——成熟度的明确边界。

7.2 量化:能省都省

GPTQ(AutoAWQ、GPTQModel)、BitsAndBytes(NF4/INT8)、LLM Compressor(FP8-W8A8、INT4-W4A16、INT8-W4A8、INT8-W8A8)、Marlin 高性能内核(GPTQ/AWQ/FP8/FP4)、FP4 系(NVFP4、MXFP4、MXFP6、MXFP8)、AMD Quark、TorchAO、Intel Neural Compressor;KV 本身也能压(FP8 KV Cache)。

7.3 硬件:CUDA 是主场,其他都在位

  • NVIDIA CUDA:主场,Ampere 起全支持;
  • AMD ROCm:v0.10.0 起 V1 默认开;
  • Intel XPU(Gaudi/GPU)、CPU(x86/ARM/Apple/IBM Z);
  • Google TPU:核心内置,文档单列;
  • AWS Trainium/Inferentia:搬出核心,独立插件 vllm-neuron(beta,基于 0.21.0)维护;
  • Apple Silicon:vLLM-Metal 插件支持。

一句话:CUDA/ROCm/XPU/CPU/TPU 住核心,Neuron 和苹果走插件;v0.12.0 起有硬件插件系统,第三方后端不用再往核心里塞。

7.4 服务与高级玩法

  • OpenAI 兼容 API:/v1/completions、/v1/chat/completions(含 batch)、/v1/responses(含 cancel)、/v1/embeddings、音频转录/翻译;另有 /rerank、/score、/classify、/tokenize、/detokenize、/generate、/pooling。
  • 结构化输出:默认 xgrammar(auto),v0.12.0 起 guided_* 统一到 structured_outputs。
  • 投机解码:EAGLE、MTP、Medusa 等;V1 用 num_tokens_with_spec 把草稿 token 纳入统一调度,只有验过的才落盘。草稿咋来、咋验,看这张图:

前 m 个全接受

第 j 个处拒

Scheduler 分配 num_tokens_with_spec
含草稿额度

Draft 模型一次生成 k 个草稿 token

Target 模型单次前向
并行验证 k 个位置

逐个位置
接受/拒绝?

采纳 m 个 token
仅这 m 个落 KV 缓存

采纳前 j-1 个
第 j 个按目标分布重采样

采样输出回流客户端

  • 前缀缓存:默认开(§4)。
  • LoRA:多 LoRA 并发,max_loras 卡上限。
  • 分离式 prefill/decode:实验性,KV 连接器把 prefill 算出的 KV 搬给 decode,NIXL、Mooncake、LMCache、offloading 等九种连接器。

8. 跟邻居比:护城河是显存效率

维度vLLMSGLangTGI (HF)TensorRT-LLM
实现语言Python 为主Python 为主Rust + PythonC++/Python(编译成引擎)
显存管理PagedAttention(块表)RadixAttention(radix 树缓存)连续分配(简单)分页 + paged KV
调度特色统一调度 + 长面包切片N+1 调度连续批处理in-flight 批处理
模型广度极广(240+ 架构)广中(HF 生态)窄(NVIDIA 认证)
硬件CUDA/ROCm/XPU/CPU/TPU + 插件NVIDIA 为主多硬件仅 NVIDIA
定位通用推理服务引擎强推理/agent 框架HF 官方服务NVIDIA 极限性能

差别是结构性的:TGI 是 HF 亲儿子,稳但并行弱;TensorRT-LLM 在 N 卡上压极限,代价是模型少、要编译、闭源;SGLang 和 vLLM 路线最近(都 Python、都分页/radix 缓存、都接 xgrammar),拼的是调度细活和生态。vLLM 的护城河就一条:PagedAttention 换来的显存效率,撑起了模型广度和并行组合。


9. 长大时间线

  • 2023-06:vLLM 首发,PagedAttention 亮相(论文还没出)。
  • 2023-09:SOSP 论文发表,2–4 倍吞吐成行业基准线。
  • 2024 下半年:连续批处理之外,补上切片预填充、前缀缓存、投机解码。
  • 2025-01:V1 引擎实验发布(VLLM_USE_V1=1),调度器/KV 管理/worker/采样/API 全重做。
  • 2025-03:v0.8.0,V1 在 CUDA 上转正(还能 VLLM_USE_V1=0 回退)。
  • 2025-05:v0.9.0,DP/EP/PD 雏形、NIXL、PyTorch 2.7 / CUDA 12.8。
  • 2025-07:v0.10.0,清 V0 代码、ROCm 默认 V1、Responses API、MXFP4 MoE。
  • 2025-12:v0.12.0,PyTorch 2.9、guided_* 并入 structured_outputs、xformers 后端退役、Model Runner V2 实验。
  • 2026 全年:约两周一发;到 v0.27.1(2026-08-11),V0 从代码树彻底删除,LLMEngine/AsyncLLMEngine 只是 V1 的别名。

写报告别写错:“V0 vs V1” 现在是历史名词。2026 年代码里没有 V0,vllm/engine/llm_engine.py 六行纸,把 LLMEngine 指回 V1。要对比只能讲 2025 年初之前的旧架子(单体 LLMEngine 调度和 worker 住一屋、vllm/core/scheduler.py、vllm/worker/worker.py),并注明"已退市"。


10. 小结: tuning 清单

起点是个局部优化——KV 从"整块连续"改成"按页管理"。但它撬动了整条链:显存不碎了,公交车式批处理才便宜;块能合租、能盖章,前缀缓存和并行采样才成立;调度不分备菜上菜了,长面包切片才顺理成章。今天它是模型广度、量化、多硬件、多并行、OpenAI 兼容、高级推理的完整引擎,地基全是 PagedAttention。

选型直说:通用、大规模、模型杂、还要 OpenAI 兼容——vLLM;锁死 N 卡追单模型极限——拉 TensorRT-LLM 来比;规模小、抱 HF 大腿——TGI 更轻;复杂推理/agent 流程——SGLang 同量级。跑顺了绕不开四个旋钮:KV 块大小、max_num_batched_tokens 预算、前缀缓存开关、切片在 TTFT 和吞吐之间那笔账。


参考来源

  • Kwon et al., Efficient Memory Management for LLM Serving with PagedAttention, SOSP 2023. arXiv:2309.06180. DOI 10.1145/3600006.3613165
  • vLLM 官方文档(docs.vllm.ai,arch_overview / v1_guide / quantization / disaggregated_prefill 等),访问于 2026-08
  • vLLM 博客:2023-06 发布帖、2025-01 V1 公告、2025-09 “Anatomy of vLLM”
  • vLLM 源码(vllm-project/vllm,commit 8151f2ad,2026-08-12):v1/engine/core.py、v1/core/sched/scheduler.py、v1/core/kv_cache_manager.py、v1/core/block_pool.py、v1/attention/ops/paged_attn.py、v1/executor/、entrypoints/openai/api_server.py
  • 版本里程碑:v0.8.0–v0.27.1 发布说明
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐