拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

vllm学习笔记之调度器/抢占/分块预填充(scheduler/preempt/chunked prefill):用 TaoToken 统一 Key 跑通配置骨架

vllm学习笔记之调度器/抢占/分块预填充(scheduler/preempt/chunked prefill):用 TaoToken 统一 Key 跑通配置骨架

1. 从一次本地推理卡顿说起:为什么要啃 vLLM 调度器

如果你在本地跑过 vLLM 的离线推理脚本,大概率遇到过这种场景:几个短 prompt 秒回,突然塞进去一个 8K 长文本,整个服务像被按了暂停键,后面排队的请求全部干等。这不是模型慢,而是调度器在 prefill 阶段被一个长请求独占了 GPU。vLLM 的 scheduler、preempt(抢占)、chunked prefill(分块预填充)这三个机制,就是专门解决这类资源竞争问题的。

这篇笔记面向正在调试本地推理服务的同学,把 vLLM 从LLM.generate()到Scheduler.schedule()的调用链拆开,重点落在三件事:请求怎么进 waiting queue、KV cache 不够时怎么抢占、长 prefill 怎么被切成 chunk 交错执行。同时我会给出一套可复制的配置骨架,并用 TaoToken 统一 Key 把模型对话和接入文档串起来,方便你在学习源码的同时有一套能跑通的验证环境。读完你应该能自己改max_num_batched_tokens、观察调度日志、判断抢占是否频繁发生。

需要先说明:vLLM 的调度逻辑在 V0 和 V1 之间有差异,本文以当前主流的 V1 引擎行为为主,涉及 V0 的地方会单独标注。源码路径以vllm/v1/core/sched/scheduler.py为参考,不同版本行号会变,但函数名和状态机基本稳定。

2. TaoToken 前置:统一 Key 与配置骨架准备

在深入调度器之前,先把验证环境搭好。学习源码时经常需要一边跑推理一边调模型接口做对照,如果每个模型都单独配一套 Key 和 base_url,配置会非常散。TaoToken 提供统一 Key,把模型对话、coding plan、API Keys 管理收敛到一个入口,适合这种「边学边验证」的场景。

你需要准备的东西不多:一个可用的 API Key,以及两个配置文件骨架。下面这份config.toml是我在本地调试时用的结构,把模型服务地址和调度相关参数分开,避免改调度参数时误动接入配置。

# config.toml - 本地推理 + 统一 Key 接入骨架 [server] host = "0.0.0.0" port = 8000 # vLLM OpenAI 兼容服务的基础地址 base_url = "http://127.0.0.1:8000/v1" [taotoken] # 统一 Key,从控制台获取后填入环境变量更安全 api_key_env = "TAOTOKEN_API_KEY" # 模型对话入口,用于对照验证 chat_endpoint = "https://taotoken.net/api" # 接入文档,排障时对照参数 doc_ref = "https://taotoken.net/api" [scheduler] # 调度核心参数,下面会逐个解释 max_num_batched_tokens = 8192 max_num_seqs = 64 enable_chunked_prefill = true gpu_memory_utilization = 0.90 preemption_mode = "recompute"

对应的settings.json用于客户端侧,把请求参数和调度观察开关放一起:

{ "model": "facebook/opt-125m", "sampling": { "temperature": 0.8, "top_p": 0.95, "max_tokens": 256 }, "scheduler_probe": { "log_schedule_each_step": true, "log_preempt_events": true, "log_chunk_boundaries": true }, "taotoken": { "api_key_env": "TAOTOKEN_API_KEY", "console_ref": "https://taotoken.net/api" } }

把 Key 写进环境变量,别硬编码进文件:

export TAOTOKEN_API_KEY="你的统一Key"

这里有个容易踩的点:base_url指向本地 vLLM 服务,而 TaoToken 的chat_endpoint用于模型对话对照。两者不要混用,本地推理走本地端口,需要对照云端模型行为时再切到统一 Key 的入口。API Keys 的创建和管理在控制台完成,接入文档里有完整的参数说明,排障时优先查文档而不是猜。

3. 可复制配置:调度器、抢占与分块预填充参数详解

这一节把配置骨架里的每个调度参数讲清楚,你改完能直接观察行为变化。

3.1 请求如何进入 scheduler 的 waiting queue

从LLM.generate()开始追。generate()内部调用_validate_and_add_requests(),再到_add_request(),最终走到llm_engine.add_request()。这条链上做了三件事:input_preprocessor.preprocess()预处理 prompt,_add_processed_request()创建Sequence和SequenceGroup,最后scheduler.add_seq_group()把请求塞进 waiting queue。

关键状态定义在SequenceStatus里:

class SequenceStatus(enum.IntEnum): WAITING = 0 RUNNING = 1 SWAPPED = 2 FINISHED_STOPPED = 3 FINISHED_LENGTH_CAPPED = 4 FINISHED_ABORTED = 5 FINISHED_IGNORED = 6

请求刚进来是 WAITING,被调度选中后通过_allocate_and_set_running()变成 RUNNING,同时block_manager.allocate()给它分配 KV block。SWAPPED 是抢占后可能进入的状态,注意注释里写的:SWAPPED 之后的状态都算 finished。

3.2 schedule() 与两种调度路径

Scheduler.schedule()是核心入口,它调用_schedule()挑选本轮执行的 sequence group,返回SchedulerOutputs,里面包含 batch、调度信息、以及哪些 KV block 需要 swap in/out/copy。

调度分两条路径:

路径函数行为
默认_schedule_default()一次性处理整个 prefill,顺序 prefill > decode
分块_schedule_chunked_prefill()拆分 prefill,交错执行 prefill 和 decode,动态调整 chunk size

_schedule_running()负责处理 running 队列,_schedule_prefills()负责 prefill 阶段的 sequence group。开启 chunked prefill 后,调度策略会先批量处理所有待执行的 decode 请求,max_num_batched_tokens还有空余时才安排 prefill,装不下就自动分块。

3.3 抢占触发条件与模式

抢占发生在「要为新的序列分配 KV block,但缓存池没有足够空闲块」时。默认在 V1 中抢占模式是 RECOMPUTE 而非 SWAP,也就是被抢占的任务丢弃 KV cache,等资源恢复后从头 prefill 重算。

为什么要有抢占?三个原因:GPU 显存和 KV cache 有限;避免头阻塞,长 prompt 不能把新请求全堵死;提升系统稳健性,动态腾缓存保证服务质量。这跟传统 GPU scheduler 处理 3D compute workload 的抢占思想类似,只不过 vLLM 抢占的是 KV cache block 资源。

调优方向很明确:提高gpu_memory_utilization给 KV 更多空间;减小max_num_seqs或max_num_batched_tokens降低每批 KV 需求;增大tensor_parallel_size或pipeline_parallel_size腾出显存。

3.4 chunked prefill 的计算量账

分块处理理论上不改变总计算量,FFN 部分每个 chunk 线性相加,总和一致。变化在 attention:以一个 8K prompt 为例,直接 prefill 复杂度 O(N²) 约 64M。切成 4 个 2K chunk 后,第一个 chunk 4M,第二个 12M,第三个 20M,依次类推。从第二个 chunk 起,每个 attention kernel 都要重新读取之前所有 token 的 KV 对。如果切成 N 个 chunk,第一个 chunk 的 KV cache 会被读 N 次,第二个 N-1 次。

所以 chunked prefill 是否划算,取决于 FFN 和 attention 的占比。prompt 越长,attention 占比越高(N² 增长),分块带来的额外读取开销越明显。但换来的是 decode 请求被优先处理,ITL 改善,GPU 利用率提升。

4. 验证请求:观察调度日志与抢占行为

配置改完,得能验证。下面这套动作可以让你直接看到调度器在干什么。

4.1 启动服务并确认 chunked prefill 生效

python -m vllm.entrypoints.openai.api_server \ --model facebook/opt-125m \ --max-num-batched-tokens 8192 \ --max-num-seqs 64 \ --enable-chunked-prefill \ --gpu-memory-utilization 0.90

启动日志里会打印类似Chunked prefill is enabled with max_num_batched_tokens=8192的行。如果没看到,说明参数没生效,检查--enable-chunked-prefill是否拼写正确。

4.2 用统一 Key 对照验证模型行为

本地服务起来后,用 TaoToken 的模型对话入口做对照,确认请求参数一致:

curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "facebook/opt-125m", "messages": [{"role": "user", "content": "Hello, my name is"}], "temperature": 0.8, "top_p": 0.95 }'

返回结构里能看到生成的 token 和 finish_reason。这一步的目的是确认你的采样参数和本地推理脚本一致,避免因为参数差异误判调度行为。

4.3 构造长 prompt 触发分块与抢占

写一个脚本,先发 16 个 4K 长度的 prompt,总 token 64K 超过 8192 的 batch 上限,观察是否被切成多轮 prefill:

from vllm import LLM, SamplingParams prompts = ["请详细解释调度器原理。" * 200 for _ in range(16)] sampling_params = SamplingParams(temperature=0.8, top_p=0.95, max_tokens=64) llm = LLM( model="facebook/opt-125m", max_num_batched_tokens=8192, max_num_seqs=64, enable_chunked_prefill=True, gpu_memory_utilization=0.90, ) outputs = llm.generate(prompts, sampling_params) for i, out in enumerate(outputs): print(f"[{i}] {out.outputs[0].text[:40]!r}")

跑的时候盯日志,重点看三类信息:每步调度的 batch 大小、是否有 preempt 事件、chunk 边界在哪里。如果看到preempt字样,说明 KV cache 不够,有任务被抢占重算。

4.4 调整参数对比抢占频率

把max_num_batched_tokens从 8192 降到 2048,再跑一次同样的脚本。理论上 batch 变小,每轮 KV 需求降低,抢占应该减少,但 TTFT 会变差。反过来调到 16384,TTFT 改善但抢占可能增多。这个对比能让你直观感受到吞吐和延迟的 tradeoff。

5. 本篇常见错排查

启动报错找不到 chunked prefill 参数:确认 vLLM 版本,老版本参数名可能是--enable-chunked-prefill或通过EngineArgs传入。V1 引擎默认行为有变化,先看版本对应的文档。

日志里 preempt 频繁刷屏:说明 KV cache 长期不足。优先提高gpu_memory_utilization,其次降max_num_seqs。如果模型本身很大,考虑开 tensor parallel。

chunked prefill 开了但 TTFT 反而变差:这是正常 tradeoff。长 prompt 被切块后,它的 TTFT 会变大,换来的是其他短请求的 ITL 改善。如果你的场景全是长请求、没有并发短请求,可以关掉 chunked prefill。

统一 Key 请求返回 401:检查TAOTOKEN_API_KEY环境变量是否导出成功,echo $TAOTOKEN_API_KEY确认非空。Key 的管理和重置在控制台完成,接入文档里有鉴权头的完整格式。

本地服务和 TaoToken 入口混淆:本地推理走127.0.0.1:8000,模型对话对照走统一 Key 入口。两者模型名可能相同但后端不同,排查问题时先确认请求打到了哪个地址。

改了 config.toml 但行为没变:vLLM 服务启动参数优先级高于配置文件,如果你用命令行传了参数,配置文件里的同名字段会被覆盖。统一在一处改。

6. 把学习笔记落成可复现配置

调度器这块最容易陷入「读源码读懂了但一跑就懵」的状态。我的做法是固定一套配置骨架,每次只改一个参数,用日志验证行为变化。max_num_batched_tokens和gpu_memory_utilization这两个是最值得反复调的,前者直接影响 chunk 切分粒度,后者决定 KV cache 池大小和抢占频率。

如果你要长期做编码类任务或者搭 Agent,建议把统一 Key 的 Coding Plan 用起来,把模型调用和本地推理的配置分开管理,避免学习环境和生产环境互相污染。接入文档里有完整的参数对照表,排障时先查文档再动配置。模型对话入口适合快速验证采样参数,API Keys 管理入口负责 Key 的生命周期,控制台则是看用量和调额度的地

返回列表