- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 —— 打开代码之前该确认的六行
- 首先,不改代码就能做到的事
- PagedAttention —— 消灭碎片化的思路
- 连续批处理调度器实际在决定什么
- Prefill 与 decode 是性质完全不同的工作
- 基准测试 —— 该固定什么,该测什么
- 真正值得动代码的地方
- 结语 —— 没有测量的调优只是品味的表态
- 参考资料
引言 —— 打开代码之前该确认的六行
每次有人问"vLLM 感觉很慢,要不要改源码",我总是先确认同样几件事:日志里有没有出现抢占警告、max_num_batched_tokens 设的是多少、前缀缓存有没有打开、GPU 显存利用率是多少、物理核心有几个,以及现在到底在测什么。
大多数情况下,答案就在这六行里。真正需要动源码的场景比想象中要少得多,即便真的需要动,值得动的地方也就那么几处。
本文就沿着这个顺序走。参照基准是 vLLM v0.26.0(2026 年 7 月 25 日 PyPI 发布,Python 3.10 以上、3.15 以下),内部结构的说明是直接阅读该 tag 的源码写成的。vLLM 大约每两周发一个次要版本,所以本文里的文件路径和参数名过几个月可能就会对不上。养成对照自己所用版本源码的习惯很有必要。
先说一件事。V0 引擎已被完全弃用。官方文档明确写着"We have fully deprecated V0",并指向 RFC #18571。网上还留存的 V0 时代调优文章,大多数现在已经不适用了。
首先,不改代码就能做到的事
按优先级排列。越靠上,收益越大、成本越低。
| 旋钮 | 改变什么 | 什么时候动它 |
|---|---|---|
gpu_memory_utilization | 用于 KV cache 的显存比例 | 看到抢占警告时。从默认值往上调 |
max_num_batched_tokens | 单个 step 处理的总 token 预算 | 决定优先保 TTFT 还是 ITL 时 |
max_num_seqs | 同时运行的请求数上限 | 显存不够或批次偏浅时 |
| 前缀缓存 | 共享前缀的 KV 复用 | 系统提示很长且被共享时 |
| 量化 | 权重与 KV cache 的字节数 | decode 受限于带宽时 |
tensor_parallel_size | 权重在 GPU 间切分的程度 | 模型放不下或 KV 空间不够时 |
| attention backend | 用哪个 kernel | 自动选择不是最优时 |
优化级别 -O0 到 -O3 | 用启动时间换稳态性能 | 开发循环还是生产环境 |
先消灭抢占
最常见的单一原因。KV cache 不够时,vLLM 会抢占正在运行的请求,之后再重新计算。如果日志里反复出现这样一行,别的调优就没有意义了。
WARNING ... Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode
because there is not enough KV cache space. This can affect the end-to-end
performance. Increase gpu_memory_utilization or tensor_parallel_size ...
V1 默认的抢占方式不是 swap,而是重新计算(recompute)。被抢占请求的 prefill 会从头再做一遍。所以抢占一旦频繁发生,吞吐量和延迟会一起崩掉。应对方式文档已经直接给出:调高 gpu_memory_utilization,或调低 max_num_seqs 和 max_num_batched_tokens,或调高 tensor_parallel_size 来增加每张 GPU 的 KV 空间。
用 token 预算权衡 TTFT 与 ITL
max_num_batched_tokens 是单个 step 能调度的 token 总量。官方文档把方向写得很明确。
- 值较小时(比如 2048)ITL 会变好,因为拖慢 decode 的 prefill 块变小了。
- 值较大时TTFT 会变好,因为一个 batch 里能塞进更多 prefill token。
- 如果目标是吞吐量,文档建议设得比 8192 大,尤其是在大 GPU 上跑小模型的情况下。
有一个坑要注意。在关闭 chunked prefill 的状态下,max_num_batched_tokens 必须大于 max_model_len,否则服务器可能在启动时就崩溃。
from vllm import LLM
# 对话式服务: 优先保证单 token 延迟
llm = LLM(model="meta-llama/Llama-3.1-8B-Instruct", max_num_batched_tokens=2048)
# 批处理: 优先保证吞吐量
llm = LLM(model="meta-llama/Llama-3.1-8B-Instruct", max_num_batched_tokens=16384)
启动时间也是性能的一部分
如果是用同一个模型、同一套配置反复启动的环境,文档给出了三种手段。
- 复用编译缓存。
torch.compile的产物会存到VLLM_CACHE_ROOT(默认是家目录下的一个缓存目录),你可以把这个目录烤进容器镜像,或者在机器之间直接复制。设置VLLM_FORCE_AOT_LOAD=1后,一旦缓存未命中就会显式报错,而不是默默重新编译。模型、配置、相关环境变量、torch 版本、GPU 型号中任何一项发生变化,缓存都会失效。 - 用
--kv-cache-memory跳过内存性能分析。 启动时 vLLM 会把能重现当前分配结果的数值打到日志里。下次启动时传入这个值就能跳过测量步骤。但这有代价:KV cache 大小从"测量得出"变成"固定为指定值",设得保守会削减并发数,设得乐观则会导致分配失败。只有在同一块 GPU、同样的初始空闲显存条件下才有效。 --enforce-eager。 把编译和 CUDA graph 捕获都跳过。启动最快,但稳态下的 decode 性能会变差。这是给开发循环用的,也可以用来衡量启动时间里编译占了多大比重。
不要饿着 CPU
一个容易被忽略、值得单独拿出来说的点。vLLM V1 是多进程架构。有 N 块 GPU,就有 1 个 API server、1 个 engine core、N 个 GPU worker,至少 N 加 2 个进程在抢 CPU。
文档把最低要求钉在物理核心这个基准上。如果开了超线程,1 个 vCPU 只等于半个物理核心,所以需要的 vCPU 数量要翻倍。engine core 进程尤其对 CPU 饥饿敏感,因为它跑的是一个忙等待循环。如果在虚拟化环境里 GPU 利用率莫名其妙偏低,应该先怀疑这里。
attention backend 默认自动选择
vLLM 会看 GPU 架构、模型和配置,从优先级列表里选出第一个兼容的 backend。以 v0.26.0 为准,标准 attention 的优先级如下。
| 架构 | 第 1 优先 | 第 2 优先 | 第 3 优先 | 第 4 优先 | 第 5 优先 |
|---|---|---|---|---|---|
| Blackwell (SM 10.x) | FLASHINFER | FLASH_ATTN | TRITON_ATTN | FLEX_ATTENTION | TURBOQUANT |
| Ampere / Hopper (SM 8.x–9.x) | FLASH_ATTN | FLASHINFER | TRITON_ATTN | FLEX_ATTENTION | TURBOQUANT |
手动切换的方式如下。指定一个不兼容的 backend 会带着原因报错。
vllm serve Qwen/Qwen3-8B --attention-backend FLASH_ATTN
# 或者用结构化配置
vllm serve Qwen/Qwen3-8B -ac.backend FLASH_ATTN
PagedAttention —— 消灭碎片化的思路
从这里开始进入内部结构。vLLM 的出发点观察很简单:如果给每个请求的 KV cache 分配一整块连续的大内存,大部分内存都会被浪费掉。
浪费分三种。因为不知道请求会不会跑到最大长度而提前预留的内部预留、实际没用到就结束的过度分配,以及归还和重新申请大小不一的内存块过程中产生的外部碎片化。
解法直接照搬了操作系统的虚拟内存。把 KV cache 切成固定大小的 block,再用一张 block table 把逻辑上连续的序列映射到物理上分散的 block 上。
请求 A 的逻辑 KV: [t0 t1 t2 t3] [t4 t5 t6 t7] [t8 t9 __ __]
│ │ │
block table A → block 7 block 3 block 12
请求 B 的逻辑 KV: [t0 t1 t2 t3] [t4 t5 __ __]
│ │
block table B → block 7 block 5
↑
如果是共同前缀,就共享同一个 block(前缀缓存)
结果有两个。第一,除了最后一个 block 剩余的那点空间,不再有浪费。原论文将其描述为"near-zero waste in KV cache memory"。第二,block 级别的共享成为可能。使用同一个系统提示的请求会物理共享前缀 block,这就是前缀缓存。论文报告,靠这两点,在同等延迟下相比 FasterTransformer 和 Orca 实现了 2 到 4 倍的吞吐量提升(Kwon et al., SOSP 2023)。
在 v0.26.0 里,这部分逻辑位于 vllm/v1/core/ 下。kv_cache_manager.py 负责按请求分配 block,block_pool.py 负责 block 池与基于哈希的复用,kv_cache_coordinator.py 负责协调同时使用多种缓存的模型(混合 attention)。
实务上的含义是这样的:前缀缓存只有在系统提示又长又被共享时才划算。 如果每个请求的前缀都不一样,剩下的就只有哈希计算和 block 管理的开销。开启前后各测一次,是唯一正确的判断方法。
连续批处理调度器实际在决定什么
vllm/v1/core/sched/scheduler.py 里的 schedule() 方法,是每个 engine step 都会执行的逻辑。源码开头的注释精确概括了这套设计。
There's no "decoding phase" nor "prefill phase" in the scheduler. Each request just has the num_computed_tokens and num_tokens_with_spec. At each step, the scheduler tries to assign tokens to the requests so that each request's num_computed_tokens can catch up its num_tokens_with_spec.
这句话是理解 V1 调度器的钥匙。调度器不区分 prefill 和 decode。每个请求只持有"到目前为止已计算的 token 数"和"应该计算到的 token 数"这两个量,每个 step 都在分配 token 让前者追上后者。靠这一个抽象,chunked prefill、前缀缓存、推测解码全都不需要特殊分支就能表达出来。
实际循环的骨架大致如下。
# vllm/v1/core/sched/scheduler.py 结构的概括(非真实代码)
def schedule(self):
token_budget = self.max_num_scheduled_tokens # = max_num_batched_tokens
# 1) 先处理 RUNNING 请求。也就是说 decode 拥有优先权。
for request in self.running:
if token_budget <= 0:
break
num_new = min(need(request), token_budget)
if not kv_cache_has_room(request, num_new):
preempt(self.running.pop()) # 从后往前抢占
continue
schedule_tokens(request, num_new)
token_budget -= num_new
# 2) 用剩下的预算接入 WAITING 请求。也就是说 prefill 靠后。
while self.waiting and token_budget > 0:
if len(self.running) >= self.max_num_running_reqs: # = max_num_seqs
break
request = self.waiting.peek()
num_new = min(need(request), token_budget)
# 放不下就切一部分放进去 → 这就是 chunked prefill
schedule_tokens(request, num_new)
token_budget -= num_new
由此可以看到,我们在配置里调整的那些值究竟插在哪里。
max_num_batched_tokens就是token_budget的初始值,是单个 step 的总工作量。max_num_seqs就是max_num_running_reqs,是运行队列长度的上限。long_prefill_token_threshold是单个 prefill 请求在一个 step 里最多能拿走多少 token 的上限,防止一个长 prompt 独占预算,饿死其他请求。
decode 优先获得分配这一点很重要。策略是先照顾已经在流式输出的用户,再用剩下的预算开始新请求的 prefill。所以负载一上升,TTFT 会先恶化,ITL 相对更扛得住。
Prefill 与 decode 是性质完全不同的工作
这两个阶段使用硬件的方式正好相反。
| 维度 | prefill | decode |
|---|---|---|
| 一次处理的 token | 整个 prompt(数千个) | 每请求 1 个 |
| 算术强度 | 高。大矩阵乘法 | 非常低 |
| 瓶颈 | 计算(tensor core) | 显存带宽 |
| 相关指标 | TTFT | TPOT、ITL |
| 加大 batch 的效果 | 已经饱和,收益很小 | 共享权重读取,收益很大 |
decode 之所以受限于带宽,原因很简单:每生成一个 token,都要把整个模型的权重读一遍。batch 为 1 时,这次读取只换来 1 个 token;batch 为 64 时,同样这次读取能换来 64 个。所以 decode 的吞吐量几乎与 batch 大小成正比增长,直到贴上带宽的墙。
问题在于,把这两个阶段分开跑,两边都吃亏。只跑 prefill 的 step 里,tensor core 饱和,内存管道闲着;只跑 decode 的 step 里则正相反。
chunked prefill 正面解决了这个问题。把长的 prefill 切开,混入与 decode 请求相同的 batch 里。计算受限的任务和内存受限的任务共存于一个 batch,两种资源就能同时被用上。在 V1 里,只要条件允许,这个功能默认就是开着的。
这里的权衡是显性的。chunk 切得小,decode 受到的干扰少,ITL 会变好,但 prefill 被拆到更多 step 里,TTFT 会变差;chunk 切得大则相反。哪一边正确,由服务本身决定,不是 vLLM 决定的。
基准测试 —— 该固定什么,该测什么
这是本文最重要的一节。前面所有的调优,都只有在测量诚实的前提下才有意义。
你正在测量的值的精确定义
vLLM 的 benchmark 工具输出的指标,定义都钉死在源码里。
| 指标 | 定义 | 谁在乎 |
|---|---|---|
| TTFT | Time to First Token。从请求发出到第一个 token | 用户体感的响应速度 |
| TPOT | Time per Output Token,不含第一个 token 的平均值 | 流式输出的体感速度 |
| ITL | Inter-token Latency。连续 token 之间间隔的分布 | 卡顿。要看尾部,不是看均值 |
| E2EL | End-to-end Latency。请求整体耗时 | 批处理作业 |
| Output throughput | 每秒输出 token 数 | 成本 |
把 TPOT 和 ITL 分开来用很重要。TPOT 是单个请求的平均值,ITL 是每一个间隔的分布。如果平均 TPOT 是 20ms,但 ITL 的 p99 是 400ms,用户感受到的会是"偶尔会卡一下"。只看均值,这个现象根本看不出来。
实际命令
# 1) 启动服务器。要调优的参数在这里固定下来。
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--max-num-batched-tokens 8192 \
--max-num-seqs 256 \
--gpu-memory-utilization 0.90 &
# 2) 施加负载。变化请求速率,打出多个点。
vllm bench serve \
--backend vllm \
--model meta-llama/Llama-3.1-8B-Instruct \
--dataset-name sharegpt --dataset-path sharegpt.json \
--num-prompts 500 \
--request-rate 8 \
--percentile-metrics ttft,tpot,itl,e2el \
--metric-percentiles 50,90,99 \
--save-result --result-filename rate8.json
# 3) 如果想知道离线吞吐量上限,用这个
vllm bench throughput --model meta-llama/Llama-3.1-8B-Instruct \
--dataset-name sharegpt --dataset-path sharegpt.json --num-prompts 1000
必须固定的东西
要相信一次测量,以下这些必须全部固定住。哪怕只有一项在变,比较就失去了意义。
- 输入与输出的长度分布。 同一个数据集、同一个随机种子。如果用合成数据,要显式固定输入长度和输出长度。长度分布一变,吞吐量可以随意变化。
- 请求到达速率。 用无限负载去测,能得到吞吐量上限,但延迟会变得毫无意义(全是排队等待时间)。一边变化请求速率一边画曲线,才是唯一有用的形式。单独一个点说明不了任何问题。
- 热身(warmup)。 最初几个请求会混入编译、CUDA graph 捕获、缓存预热的开销,必须从统计里剔除。
- 前缀缓存的状态。 如果开着缓存反复用同一个 prompt 测,从第二次请求开始 TTFT 会大幅变好。把这个当成优化成果来汇报就是在撒谎。要么清空缓存重测,要么就测缓存已预热的稳态——二选一,并保持一致。
- GPU 时钟与"邻居"。 如果是共享硬件,要确保同一时间段没有别的作业在跑。一旦触及功耗上限,时钟频率就会下降。
- 版本。 把 vLLM、PyTorch、驱动、镜像标签都和结果一起记下来。
当作曲线来读
与其看单点数字,不如把请求速率依次提到 5、10、15、20 再测,会得到这样的形状。
p99 TTFT
^
| ╱ ← 队列从这里开始堆积
| ╱
| ______╱
| ____________________╱
+--------------------------------------> 请求速率(req/s)
拐点(knee)
运行点应该落在拐点的左侧。
拐点右侧是"吞吐量出得来,但延迟已经失控"的区域。
调优的目标不是最大吞吐量,而是满足 SLO 前提下的最大吞吐量。如果条件是 p99 TTFT 不超过 500ms,那么在守住这个条件的前提下能跑出的请求速率,才是真正的指标。一边变化 max_num_batched_tokens 一边画出多条这样的曲线,哪个值适合你的服务,一眼就能看出来。
想定位瓶颈的时候
数字不好看又不知道原因时,就该上性能分析器(profiler)了。不过文档先给出了警告:性能分析是给开发者用的,会明显拖慢推理速度,终端用户绝不能开启它。 如果需要低开销,用 Nsight Systems;如果连堆栈和张量形状都需要,就用 PyTorch profiler。
# 给服务器挂上 profiler 再启动(--profiler-config 是 v0.13.0 及以上版本)
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--profiler-config '{"profiler": "torch", "torch_profiler_dir": "./vllm_profile"}'
# 只截取某个区间来收集
curl -X POST http://localhost:8000/start_profile
# ... 只发送几个请求。trace 会变得非常大 ...
curl -X POST http://localhost:8000/stop_profile
# 也可以配合 benchmark 一起用
vllm bench serve --backend vllm --model ... --profile --num-prompts 2
收集到的 trace 用 Perfetto UI 查看。把请求数控制得少一点很重要。文档提到,在 70B 级别的模型上导出 100 个请求的 trace,在 H100 上大约要花 10 分钟。
真正值得动代码的地方
以上都做完了还是不够的话,那就该看源码了。现实中值得下手的地方一共三处。
1. 自定义 logits processor —— 最安全的位置
推荐它的理由是不用改源码,以插件形式挂上去,不需要 fork vLLM。logits processor 是按 batch 为单位工作的:接收一个形状为"请求数 x 词表大小"的 logits 张量,做变换,再交给 softmax。
继承 vllm.v1.sample.logits_processor.LogitsProcessor,实现五个方法。
# my_pkg/procs.py
import torch
from vllm.config import VllmConfig
from vllm.sampling_params import SamplingParams
from vllm.v1.sample.logits_processor import BatchUpdate, LogitsProcessor
class BanTokenAfterN(LogitsProcessor):
"""输出超过 N 个 token 后禁用某个特定 token(示例)。"""
@classmethod
def validate_params(cls, params: SamplingParams):
# 在入口处提前过滤掉错误的参数。不实现这个的话,
# 异常值会原样一路流进 kernel。
v = params.extra_args and params.extra_args.get("ban_after")
if v is not None and not isinstance(v, int):
raise ValueError("ban_after must be int")
def __init__(self, vllm_config: VllmConfig, device: torch.device,
is_pin_memory: bool):
self.device = device
# batch 索引 -> (禁用的 token、阈值、输出 token 列表的引用)
self.req: dict[int, tuple[int, int, list[int]]] = {}
def is_argmax_invariant(self) -> bool:
# 可能会改变 argmax 对应的 token,所以是 False。
# 设为 True 的话,只要整个 batch 都是贪心采样,vLLM 就会整个跳过这个 processor。
return False
def update_state(self, batch_update: BatchUpdate | None) -> None:
if batch_update is None:
return
# 必须按 removed -> added -> moved 的顺序处理。
for idx in batch_update.removed:
self.req.pop(idx, None)
for idx, params, _prompt_ids, output_ids in batch_update.added:
self.validate_params(params)
n = params.extra_args and params.extra_args.get("ban_after")
if n is None:
self.req.pop(idx, None)
else:
# output_ids 是一个活的列表引用,
# 所以每个 step 都能看到最新的输出。
self.req[idx] = (params.extra_args["ban_token"], n, output_ids)
for a, b, direction in batch_update.moved:
va, vb = self.req.pop(a, None), self.req.pop(b, None)
if vb is not None:
self.req[a] = vb
if va is not None and direction.name == "SWAP":
self.req[b] = va
def apply(self, logits: torch.Tensor) -> torch.Tensor:
for idx, (tok, n, out_ids) in self.req.items():
if len(out_ids) >= n:
logits[idx, tok] = float("-inf") # in-place 对内存更友好
return logits
挂载方式有两种:传入完整的类名,或者注册为包的 entrypoint。
vllm serve facebook/opt-125m --logits_processors my_pkg.procs:BanTokenAfterN
# pyproject.toml —— 只要安装好就会自动加载
[project.entry-points."vllm.logits_processors"]
ban_after = "my_pkg.procs:BanTokenAfterN"
有三点需要注意。
- logits processor 的集合在引擎初始化时就固定了。 无法按请求事后添加。要按请求开关某个 processor,唯一的办法是在
apply内部根据SamplingParams.extra_args来判断处理。 is_argmax_invariant()必须诚实回答。 设为 true 的话,只要整个 batch 都在做贪心采样,就能整体跳过、白得一份加速;但如果一个实际会改变 argmax 的 processor 返回了 true,就会悄无声息地产出错误结果。apply在每个 step 都会针对整个 batch 执行一次。 用 Python 循环遍历请求数,循环本身就会成为瓶颈。能用张量运算向量化的地方就应该向量化。- 文档自己也明确写着,这个 API "design changes are still in progress and the API may change in the near future"。使用时最好先把版本固定下来。
2. 自定义 attention backend —— 价值大,代价也大
vllm/v1/attention/backends/ 目录下是各种 backend,公共接口在 vllm/v1/attention/backend.py。v0.26.0 里包含 flash_attn.py、flashinfer.py、triton_attn、flex_attention.py,以及若干 MLA 专用实现。
值得自己动手写的场景很窄:需要标准 attention 的某个变体、现有 backend 里没有、而且这个变体对性能是决定性的。比如有一种领域特有的稀疏 mask,只需要计算完整 attention 的 10%。
代价要老实地看待。一个 backend 要把元数据构建器、CUDA graph 兼容性、prefill 和 decode 两条路径、与 chunked prefill 的交互、与前缀缓存的交互全部对齐,光写一个 kernel远远不够。而且这套接口每次 vLLM 发版都会变。
3. 调度器策略 —— 最后的手段
vllm/v1/core/sched/ 目录下有 scheduler.py、interface.py、request_queue.py。想用领域规则改写请求优先级时(比如付费用户优先、短请求优先),就会碰到这里。
不过要讲究顺序。vLLM 本身已经提供了请求优先级功能,应该先确认能不能用这个功能表达你的需求。改调度器是风险最高的选项。正如前面所见,这段代码同时与 token 预算、抢占、chunked prefill、前缀缓存、推测解码纠缠在一起。
追随上游的代价
这三个位置有一个共同的代价:fork 出来的 vLLM 会自动老化。
vLLM 大约每两周发一次小版本,期间会加入新模型支持、新 kernel、新量化格式、性能改进。fork 放着不管半年,你就用不上最新模型,用不上最新的 attention kernel,还得一直背着那些上游早就修好的 bug。到那时候,rebase 的成本会变成最初那次修改成本的好几倍。
所以代价的排序是这样的。
| 方式 | 追踪上游的成本 | 什么时候用 |
|---|---|---|
| 只调整配置参数 | 无 | 永远优先尝试 |
| 插件(logits processor 等) | 低。只在 API 变化时才有成本 | 大多数定制化需求 |
| 向上游提 PR 并被合并 | 需要花评审时间,之后为 0 | 如果是普遍有用的功能,这是最优选 |
| fork 后自己维护 patch | 高。每次发版都要 rebase | 真的没有别的办法时 |
我想着重强调第三行。如果你需要的功能对别人也有用,推给上游就是最便宜的维护策略。合并的那一刻,维护成本就归零了。
结语 —— 没有测量的调优只是品味的表态
本文讲述的顺序,本身就是结论。先消灭抢占日志,用 token 预算权衡 TTFT 和 ITL,不要饿着 CPU,测一测前缀缓存是不是真的划算。做完这些之后,才有必要去理解 PagedAttention 和调度器的行为;理解了这些之后,才知道该把代码改在哪里。
而在这整个顺序的任何一个阶段,都不能跳过"前后对比测量"这一步。如果把 max_num_batched_tokens 从 8192 调到 16384,吞吐量涨了 12%,但 p99 TTFT 翻了一倍,那这到底是改善还是退步,只有你服务的 SLO 能给出答案。没有数字支撑就做这种判断,不是调优,而是品味的表态。
最后一句话。vLLM 里用得最多的优化手法,至今仍然是"把 batch 调大",而大多数团队还没用完这个空间,就急着去打开源码了。 请先确认那六行。
参考资料
- vLLM 优化与调优指南: https://docs.vllm.ai/en/latest/configuration/optimization.html
- vLLM V1 用户指南(含 V0 弃用公告): https://docs.vllm.ai/en/latest/usage/v1_guide.html
- vLLM 自定义 logits processor 文档: https://docs.vllm.ai/en/latest/features/custom_logitsprocs.html
- vLLM attention backend 文档: https://docs.vllm.ai/en/latest/design/attention_backends.html
- vLLM 性能分析文档: https://docs.vllm.ai/en/latest/contributing/profiling.html
- vLLM 基准测试 CLI: https://docs.vllm.ai/en/latest/cli/bench/serve.html
- Kwon et al., Efficient Memory Management for LLM Serving with PagedAttention (SOSP 2023): https://arxiv.org/abs/2309.06180
- Agrawal et al., Sarathi(chunked prefill 的理论依据论文): https://arxiv.org/pdf/2308.16369
- vLLM 源码(调度器): https://github.com/vllm-project/vllm/blob/main/vllm/v1/core/sched/scheduler.py