§ 00 · PrologueThe territory charted.
vLLM 不是单纯的 inference engine — 它发明了"PagedAttention"这个抽象。把 KV cache 像虚拟内存一样分页管理,让吞吐量和拼批策略解耦于物理 GPU 内存碎片。这是它从 2023 年发布到 2026 年仍然是事实标准的根本原因。这次精读关注它 2024 年完成的 v1 重构 — 把原本的单体引擎拆成可插拔的 EngineCore + Worker,并提供进程内 / 独立进程 / Ray actor 三种部署模式。
跟 SGLang 的对比是这份文档的潜流。SGLang 用 RadixCache 在请求间共享 prefix;vLLM 用 BlockPool 在物理内存里共享 block。两条路径走到了相似的终点(prefix caching),但底层的抽象层级不同 — 这造就了两个系统在多轮对话、结构化生成、多模态扩展上的不同表现。
§ M0 · 25 minThe repo, at first sight.
vLLM 是个 monorepo — 顶层就有 vllm/(Python 包)+ csrc/(C++/CUDA/HIP 源码)+ cmake/(构建系统)+ tests/ + benchmarks/ + docs/。没有像 SGLang 那样把 kernel 拆成独立 wheel — vLLM 的策略是"一个 wheel 内置所有 platform 的 CUDA/HIP 二进制"。
vllm/ 主包里有几个值得立刻定位的 file:
| File / Dir | Role |
|---|---|
vllm/v1/ | ★ 2024 重构后的新架构 — 整个内核都在这 |
vllm/engine/ | 老 v0 入口(llm_engine.py · async_llm_engine.py · arg_utils.py) |
vllm/_aiter_ops.py | AMD AITER 自定义算子包装 |
vllm/_custom_ops.py | 通用 C++ ext 算子包装(量化、norm、rotary etc.) |
vllm/_xpu_ops.py | Intel XPU 算子 |
vllm/attention/ | 老 v0 attention 框架(v1 在 v1/attention/) |
vllm/model_executor/ | model 定义 + 加载(Llama / DeepSeek / Kimi / ...) |
vllm/distributed/ | TP/PP/DP 通信 + KV connector(NIXL · offloading) |
vllm/compilation/ | torch.compile 集成 + custom passes |
vllm/platforms/ | CUDA / ROCm / TPU / XPU / CPU 平台抽象 |
vllm/kernels/ | 纯 Python kernel 封装 |
vllm/entrypoints/ | OpenAI 兼容 API server + offline LLM 类 |
csrc/ | extensive C++/CUDA/HIP 源码 |
vllm/v1/ 进一步切分:
| v1 subdir | What |
|---|---|
v1/engine/ | EngineCore(core.py 2161 行)· AsyncLLM · LLMEngine |
v1/core/ | KVCacheManager · BlockPool · sched/Scheduler |
v1/worker/ | GpuModelRunner(7185 行!)· CPU/TPU/XPU runner |
v1/attention/ | 20 个 attention backend(含 3 个 ROCm) |
v1/kv_offload/ | ★ KV cache offload 到 CPU / disk / NIXL(disagg serving) |
v1/spec_decode/ | 投机解码 |
v1/structured_output/ | JSON / regex 约束 |
v1/sample/ | 采样(top_k / top_p / typical) |
vLLM 把"什么调度(Scheduler · KVCacheManager · 在 EngineCore 里)"和"怎么执行(Worker · ModelRunner · 在独立进程或 Ray actor 里)"完全分开。这是它跟 SGLang 最显眼的设计差异 — SGLang 是每个 TP rank 都是一个完整 scheduler(N 个 scheduler 进程),vLLM 是一个 scheduler 协调 N 个 worker。
vLLM 路径的好处:scheduler 状态单点权威,KVCacheManager 不用同步;代价:scheduler 是单点瓶颈,必须做得很快(这也是为什么有 7185 行的 gpu_model_runner,把工作尽量推到 worker 端做)。
§ M1 / M2 · 50 minEngineCore — three lives.
vllm/v1/engine/core.py(2161 行)是 vLLM v1 的心脏。它定义了一个清晰的继承体系,让同一个 EngineCore 可以以三种部署形态运行:
class EngineCore: # line 91 · 基类,纯逻辑
"""Scheduler + KVCacheManager + Workers · 同步接口"""
class EngineCoreProc(EngineCore): # line 810 · 独立进程
"""Subprocess 形态 · 与 AsyncLLM 通过 ZMQ 通信"""
class DPEngineCoreProc(EngineCoreProc): # line 1626 · 数据并行
"""DP 拓扑下 N 个 EngineCoreProc 协作"""
class EngineCoreActorMixin: # line 1985 · Ray actor 行为
class EngineCoreActor(EngineCoreActorMixin, EngineCoreProc): # line 2133
class DPMoEEngineCoreActor(EngineCoreActorMixin, DPEngineCoreProc): # line 2110
这意味着用户可以选 3 种部署:
| Deployment | Class | When |
|---|---|---|
| In-process (sync) | EngineCore | Python 脚本里 LLM(...) 离线推理 |
| Subprocess (async) | EngineCoreProc | OpenAI API server 默认 |
| Ray Actor | EngineCoreActor | 多机多 GPU 部署 |
三种部署共享同一份核心逻辑 — 这是 OO 多态在系统工程里的漂亮应用。
AsyncLLM — 异步客户端
vllm/v1/engine/async_llm.py(1107 行)是 OpenAI-compatible server 用的高级入口。它做的事:
- fork 一个
EngineCoreProc子进程 - 建立 ZMQ socket(PUSH/PULL + PUB/SUB 混用)
- 提供
async def generate(prompt)async 生成器接口 - 每次拿到 EngineCore 的一批 outputs,把它们 demux 给等待的 async 请求
跟 SGLang 的 TokenizerManager 类似,但 vLLM 把 tokenizer / detokenizer 都合在 EngineCore 进程里(不是单独进程)— 这是 vLLM 整体进程数少的原因。
§ M5 · 50 minPagedAttention — virtual memory for KV.
这是 vLLM 的原创贡献。把整个 KV cache 物理内存切成定长 block(默认 16 token 一个 block),用一个"虚拟 → 物理"映射表(block table)让每个 sequence 看到的是逻辑连续的 KV,但物理上可以散布在内存任何位置。
v1 里的实现拆成三个类:
| Class | File / Line | Role |
|---|---|---|
BlockPool | v1/core/block_pool.py:130 | 物理 block 池 — 哪些 block 空闲 / 被引用 |
BlockHashToBlockMap | v1/core/block_pool.py:34 | ★ block hash → block id — 这是 prefix caching 的核心 |
KVCacheBlocks | v1/core/kv_cache_manager.py:22 | 一个请求拥有的 block 集合 |
KVCacheManager | v1/core/kv_cache_manager.py:106 | scheduler 用的 high-level API |
KVCacheCoordinator | v1/core/kv_cache_coordinator.py | 多种 cache(attn / mamba)的协调 |
Prefix caching — vLLM 的方式
跟 SGLang 的 RadixCache(树形)不同,vLLM 用哈希 + 不可变 block实现 prefix caching:
def compute_block_hash(tokens):
# 一个 block 的 hash 是它自己的 tokens + 父 block 的 hash
return hash((parent_block_hash, tuple(tokens)))
# 当新请求来时:
for i, block_tokens in enumerate(tokenize(prompt).blocks):
h = compute_block_hash(block_tokens)
if h in block_hash_to_block_map:
# cache hit — 直接复用物理 block
reuse(block_hash_to_block_map[h])
else:
# cache miss — allocate 新 block,记录 hash
new_block = block_pool.allocate()
new_block.hash = h
block_hash_to_block_map[h] = new_block
关键差异:block 的 hash 包含父 block 的 hash — 这让 hash 隐含了"我前面所有 token 的精确历史"。两个请求只有当 prefix 完全一致时,它们的 block hash 才会一样。
SGLang RadixCache(树):节点显式持有 token 序列,match_prefix 沿树查最长匹配。优点:能匹配任意长度的部分 prefix,分裂粒度细。缺点:树维护成本高,需要 ref_count + LRU。
vLLM BlockPool(hash):固定 16-token block,每个 block 一个 hash。优点:实现极简,lookup O(1),整 block 复用对 attention kernel 友好。缺点:粒度粗 — prefix 必须是 16 的倍数才能 cache hit。
实际效果:在长 prompt + 多轮对话场景,两者差距很小(因为 prefix 一致性通常是 hundreds-of-tokens 级别);但 SGLang 在短 prompt 部分共享场景下更灵活。
block_table 把它们映射到物理 BlockPool 里的 block id。Block 7 和 12 被两个请求共享(ref_count=2)— 这就是 prefix caching 在 vLLM 里的实现。当 ref_count 归零,block 回到 free pool 可被新请求用。整个机制类似操作系统页表 + 写时复制(COW),所以叫"PagedAttention"。
§ M4 · 40 minThe Scheduler — budget keeper.
vllm/v1/core/sched/scheduler.py 是 2234 行的单类。Scheduler(SchedulerInterface) 在 line 62。它的核心数据结构出乎意料地简单:
class Scheduler(SchedulerInterface):
def __init__(self, ...):
self.waiting = create_request_queue(self.policy) # 等待 prefill 的请求
self.running: list[Request] = [] # 正在 generation 的请求
# + KVCacheManager, max_num_running_reqs, token_budget, etc.
schedule() 方法(line 310)是每个 step 调用一次的核心。它做两轮调度决策:
- Phase A — running 请求继续 decode(line 347-462)
遍历self.running,给每个请求 1 个 decode token 的预算。如果总 token budget 用完 / KV cache 不够,preempt 最低优先级的请求(line 442:self.running.remove(preempted_req))。 - Phase B — waiting 请求 admit + prefill(line 529-765)
从self.waiting拉新请求,尝试 chunked prefill — 一次 step 不跑完整个 prompt,分多个 chunk 边 prefill 边和 decode 混跑。
这种"Phase A + Phase B 在同一 step 里混跑"是 vLLM continuous batching 的精髓。"continuous" 不是"不停跑",而是"prefill 和 decode 在 batch 维度共存"。
Preemption — 当资源不够
line 910 的 _preempt_request:
def _preempt_request(self, request, timestamp):
# 把所有 KV blocks 还回 BlockPool (但保留 hash → 后续可能复用)
self.kv_cache_manager.free(request)
# 状态变 WAITING,重新插到 waiting 队列前部
request.status = RequestStatus.WAITING
self.waiting.prepend_request(request)
Preempted 请求会丢失它的 KV cache(虽然 prefix block 因为有 hash 可能在 BlockPool 里被新请求复用,从而部分恢复)。下次被 schedule 时要重新跑 prefill — 这是 vLLM 在内存受限场景下的 graceful degradation。
§ M6 · 40 minGpuModelRunner — seven thousand lines.
vllm/v1/worker/gpu_model_runner.py 是整个 vLLM codebase 里最大的单一文件 — 7185 行。它干的事:
- 从 EngineCore 收
SchedulerOutput(含每个请求的 block_table、要跑的 token 数、采样参数) - 构造
ForwardContext(含 attention metadata、cuda graph bucket key 等) - 把 token 张量准备好(concat 所有请求的 input_ids、build position_ids、mask、block table padding)
- 选择 attention backend(FlashAttention / FlashInfer / Triton / ROCm AITER / ...)调它的
init_forward_metadata() - 调
model.forward(input_ids, positions, attn_metadata, kv_caches) - 跑 sampling(top_k / top_p / temperature / 结构化生成 mask)
- 把 token 写回 KV cache(kernel-level)+ append 到 block
- 构造
ModelRunnerOutput还给 EngineCore
它为什么这么大?因为它要支持:
- CUDA Graph capture(多个 batch size bucket)+ 不 capture 的 fallback
- 不同模型架构(dense / MoE / Mamba / 多模态)的 forward 差异
- LoRA / 多 adapter 切换
- Speculative decoding(draft + verify 两步走)
- EP (Expert Parallel) MoE 的 all-to-all 通信
- Mixed-precision (FP16 / BF16 / FP8 / INT4) 路径
每多一种"feature"都给这个类加几百行。这是大部分 inference engine 都会遇到的"巨型类陷阱"。vLLM 走的路是用 Mixin 拆(ec_connector_model_runner_mixin / kv_connector_model_runner_mixin / lora_model_runner_mixin)— 跟 SGLang Scheduler 是同一种应对策略。
§ M7 / M8 · 45 minBackends, twenty of them.
vllm/v1/attention/backends/ 有 20 个 attention 实现,全部实现同一个 AttentionBackend 抽象(backend.py)。registry.py 维护名字 → class 映射,selector.py 自动选择(根据模型架构和硬件)。
| Family | Files | Lines |
|---|---|---|
| Flash family | flash_attn, flash_attn_diffkv, flashinfer | 1236 + 321 + 1969 |
| Triton | triton_attn | 774 |
| Flex | flex_attention | 1217 |
| ROCm | rocm_aiter_fa · rocm_aiter_unified_attn · rocm_attn | 1471 + 304 + 545 |
| CPU | cpu_attn | 545 |
| State space | mamba_attn, mamba1_attn, mamba2_attn, gdn_attn, linear_attn, short_conv_attn | 588 + 64 + 171 + 475 + 93 + 34 |
| Quantized | turboquant_attn | 906 |
| MLA | mla/(子目录) | 多文件 |
ROCm 三件套
vLLM 在 AMD 上有三个 attention backend,对应三种策略:
| Backend | What | When |
|---|---|---|
rocm_aiter_fa.py (1471 行) | ★ 调 AITER 的 FlashAttention kernel(CK 实现) | MI300X+ · 默认主力 |
rocm_aiter_unified_attn.py (304) | AITER unified attention(统一 prefill+decode 入口) | 较新模型 |
rocm_attn.py (545) | ROCm 通用 fallback(不依赖 AITER) | 老硬件 / AITER 不支持的形状 |
跟 SGLang 的单一 aiter_backend.py(3284 行)对比 — vLLM 把 AITER 调用拆得更细,可读性更好,但加新 ROCm-specific 优化时要看清楚改哪一个。
vLLM 的 ROCm 支持比 SGLang 更结构化 — 它把 AITER 包装在 vllm/_aiter_ops.py(顶层 module)里,attention 层只是消费者。改 AITER 集成时改 _aiter_ops.py 一处即可,attention backend 自动跟着走。SGLang 是把 AITER 调用直接写在 attention backend 里,更扁但耦合更深。
对你的工作:在 vLLM 上加 AITER 新 kernel 时,先改 vllm/_aiter_ops.py 暴露 API,再在 3 个 ROCm backend 里选择消费方式。
§ Reefs · fiveWhat to avoid running aground.
读 vLLM 时最容易触礁的几处。
vllm/engine/(老 v0)和 vllm/v1/engine/(新 v1)都还在 codebase 里。看老 issue / 老 blog post 时引用的 LLMEngine 可能是 v0 那个,跟你实际在跑的 v1 EngineCore 完全不同对象。改代码前先 grep "VLLM_USE_V1" 确认 codepath。
BlockPool 的 prefix caching 依赖每个 block 的 hash 包含父 block 的 hash。如果你改 block_size(默认 16),所有现存的 cache 都失效,因为 hash 计算变了。重启 vLLM 服务后必然 cold start。改 block_size 是个有副作用的运维操作。
_preempt_request 释放 KV blocks 回 pool,但BlockPool 可能在下次 schedule 时已经把这些 block 分配给别的请求了。所谓"hash 仍在 map 里"只在 block 还没被复用前才有效。preemption + 高 KV 压力的场景下,"复用率"是个统计平均,不是保证。
7185 行的 GpuModelRunner 用了 5+ 个 mixin(kv_connector_model_runner_mixin / lora_* / ec_connector_*)。grep 某个方法时可能找不到 — 它在 mixin 里。读 forward path 时一定要先看 class GpuModelRunner(...) 的所有父类。
用户不指定时 v1/attention/selector.py 会根据模型架构 + 硬件自动选 backend。在 MI300X 上跑 Llama-3 默认会选 rocm_aiter_fa,但跑 DeepSeek-V3(MLA)可能选 triton_attn 或 fallback。跑 benchmark 时务必打印实际选中的 backend,不要假设。
§ AMD bearings · for JhinWhere to start touching.
三个具体起手点,对应你 multi-agent kernel optimization 的两个 ultimate goal。
1 · 在 vLLM 上跑 MI300X 性能 baseline
vLLM 的 ROCm 路径比 SGLang 更结构化 — 适合做横向对比基准:
# 用 vLLM 跑同一个 model 做参照
VLLM_USE_V1=1 vllm serve meta-llama/Llama-3-70B \
--tensor-parallel-size 8 \
--attention-backend rocm_aiter_fa \
--enable-prefix-caching \
--max-num-batched-tokens 8192
# 同样的 model 在 SGLang 上
python -m sglang.launch_server \
--model meta-llama/Llama-3-70B \
--tp 8 \
--attention-backend aiter
用两边自带的 benchmarks/ 跑同一组 trace,对比 throughput / TTFT / ITL。这是你 AMD performance database 的第一行数据。
2 · 改 _aiter_ops.py 加你的新 kernel
当你 multi-agent 系统跑出一个比当前 AITER kernel 更快的 attention variant,vLLM 集成路径只需改一个文件:
# 添加一个新的 wrapper
def my_faster_attn(q, k, v, ...):
"""Custom AITER attention kernel optimized by MAS."""
from my_kernels import fast_attn_v2
return fast_attn_v2(q, k, v, ...)
然后在 rocm_aiter_fa.py 的 forward_extend / forward_decode 里替换调用。这种"先暴露 API,再选择消费方"的两层结构让 kernel 实验非常快。
3 · 抄 vLLM 的 _aiter_ops.py 模式做你自己的 kernel package
SGLang 的 sgl-kernel 是独立 wheel 包;vLLM 的 _aiter_ops.py 是顶层 module 层级的 wrapper。两种都是把"kernel"和"使用 kernel 的代码"解耦的有效手段。对你的 multi-agent kernel optimization 系统:
- 短期:用
_aiter_ops.py这种顶层 wrapper 模式 — kernel 改进无需碰各个 backend 文件 - 长期:做成独立 wheel 包(学 sgl-kernel),让 vLLM / SGLang / 其他 inference engine 都能直接 import
这是 leverage 你工作的双层路径 — 先在一个引擎上做出收益,再独立发行让生态都受益。
§ EpilogueThree readings, one map.
File № 001(SkyPilot)讲的是"如何把工作丢到任何云上的任何 GPU"。File № 002(SGLang)讲的是"在那个 GPU 上如何高效跑 LLM 推理"。File № 003(vLLM)也讲推理 — 但用了不同的抽象。这三份合起来读,正好覆盖你 AMD 工作的完整栈:
- Orchestration 层(SkyPilot)— 让 multi-agent kernel optimization 系统能在多 MI300X 节点上跑分布式实验
- Inference 层 A(SGLang)— 你目前的主力 serving 引擎,了解它的 scheduler / RadixCache 让你能改 / 加 backend
- Inference 层 B(vLLM)— 对照系统 + 第二个测试床,跑 baseline + 验证你的 kernel 通用性
三个系统在工程上有同构的设计模式:(1) 把请求 / 任务 / 配置做成不可变值对象;(2) 把状态 / 物理资源做成可写实体并集中管理;(3) 用异步消息总线解耦组件;(4) 把调度决策和真正干活分到不同进程;(5) 用plugin / abstract base class 让平台扩展低成本。这五点的反复出现告诉你 — 现代分布式 ML 系统在收敛于一组共同的工程姿态。值得把这套姿态吸收进你即将搭的 multi-agent kernel optimization 系统。
— Fin de trilogie.