Home vLLM 请求处理全流程:从 HTTP 到 Scheduler、GPU 与流式输出
Post
Cancel

vLLM 请求处理全流程:从 HTTP 到 Scheduler、GPU 与流式输出

本文按固定源码版本逐段跟踪一个 vLLM 请求。先用 30 秒建立全局路径,再按模块解释问题、Input、实现、Output、性能收益、代价与正确性不变量。

另一篇:SGLang 请求全流程 · 调度器源码详解 · 面试题系列 · 源码实验手册

30 秒全流程概览

1
2
3
4
5
6
7
8
9
10
HTTP / OpenAI Chat Completion
  -> OpenAIServingChat:协议校验、chat template、tokenize
  -> AsyncLLM:建立 request ID 和本地输出 collector
  -> EngineCoreClient / IPC:把 EngineCoreRequest 送入 core 进程
  -> EngineCore + Scheduler:waiting/running、token budget、KV admission
  -> KVCacheManager / BlockPool:prefix lookup、paged KV 分配与回收
  -> Executor / GPUWorker / ModelRunner:packed input、attention、forward、sample
  -> Scheduler.update_from_output():提交 accepted token、停止判断、释放资源
  -> OutputProcessor:增量 detokenize、按 request ID 投递结果
  -> JSON / SSE:客户端看到首 token 和后续 delta

一句话理解:API 进程拥有客户端语义,EngineCore/Scheduler 拥有逻辑请求与资源决策,rank-local Runner 拥有设备执行状态;SchedulerOutput 是一次执行计划,ModelRunnerOutput 是待提交结果,二者必须严格配对。

下面进入完整源码路径。文中的类、函数和状态均固定到文章开头声明的 fork commit;伪代码用于压缩控制流,不替代链接中的源码条件分支。


本文固定到已运行验证的 fork commit d1d2f7535。 该提交基于 3b39fd284,只增加默认关闭的 VLLM_FLOW_TRACE 注释与日志,不改变推理语义。

本文是生命周期骨架:沿一个 OpenAI Chat Completion 请求,跟踪它如何从 JSON 变成 token、内部请求、 调度计划、GPU tensor、采样结果、增量文本,最后释放 KV Cache。需要深入某一模块时,进入下面的独立 模块文档;模块文档按当前源码逐项说明问题、Input、实现、Output、性能因果、代价和验证方法。

需要把源码机制和真实性能现象对应起来时,继续阅读 ../VLLM_SGLANG_SOURCE_LABS.md:其中用 A100 实验深入验证 causal prefill、prefix/Beam KV、continuous batching、chunked prefill、CUDA Graph、NGRAM 和 KV preemption。

阅读方式:先主线,再模块

学习模块它解决的核心问题深入文档
请求与输出管线外部协议如何变成稳定 engine contract,异步结果如何回到正确客户端modules/REQUEST_PIPELINE.md
Scheduler变长请求如何共享 token/KV budget,并冻结为一轮可执行计划modules/SCHEDULER.md
Paged KV/Prefix Cache动态 KV 如何分页、共享、淘汰和在部分命中时 CoW;V1 beam 如何复用 KVmodules/KV_CACHE.md
Executor/ModelRunnerSchedulerOutput 如何变成 packed tensor、attention metadata、forward 和 samplemodules/MODEL_EXECUTION.md
Speculative Decodingproposal、target verify、acceptance、KV 回滚如何跨轮协作modules/SPECULATIVE_DECODING.md

每个模块统一回答:

1
2
3
为什么需要 -> 上游 Input/不变量 -> 源码实际步骤/数据结构
-> 给下游的 Output/新不变量 -> 改变了什么物理开销
-> 改善哪个指标 -> 代价/退化条件 -> 如何实验验证

源码链接的作用是约束实现细节:字段、状态推进、异步边界和特殊条件均以固定 commit 的实际代码为准, 而不是用通用 LLM serving 概念推测当前版本行为。

导航

0. 阅读约定与性能指标

0.1 本文讨论的运行路径

  • API 层:OpenAI-compatible /v1/chat/completions
  • Engine:vLLM V1 engine,而不是已经移除或兼容保留的旧 engine 叙述。
  • GPU 执行:优先解释本机验证实际选择的 V2 GPU ModelRunner;必要处指出 V1 runner 的同构职责。
  • 普通 decoder-only generation 为主,同时说明 multimodal、structured output、LoRA、KV connector 在哪里进入相同生命周期。

0.2 性能结果不能只写“更快”

后文使用以下指标区分不同优化的真实目标:

指标定义主要受谁影响
TTFT请求到第一个可见 token 的时间排队、tokenize、prefix hit、prefill、首 token 回传
ITL相邻输出 token 的时间间隔decode batch、调度间隔、kernel、通信、输出回压
TPOT每个输出 token 的平均时间decode 与 speculative acceptance
tokens/s实例单位时间完成的 token 数continuous batching、并行度、kernel、batch shape
p95/p99尾延迟长 prefill、抢占、graph miss、队列、慢客户端
goodput满足 SLO 的请求或 token 吞吐吞吐和尾延迟的共同结果
KV usageKV block 使用率与可回收率page size、prefix reuse、序列长度、抢占

源码控制流已通过真实 HTTP 请求验证,详见 RUNTIME_VALIDATION.md。该验证使用 eager/小模型配置用于看清流程, 不是生产性能 benchmark;本文不会把一次功能验证伪装成固定加速比。

1. 先看全局:进程、对象和状态所有权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
API process
  FastAPI router
    -> OpenAIServingChat
    -> Renderer / tokenizer
    -> AsyncLLM
    -> OutputProcessor + per-request collector
            | EngineCoreRequest / EngineCoreOutputs
            v
EngineCore process
  EngineCoreProc
    -> EngineCore
    -> Scheduler
    -> KVCacheManager / BlockPool
    -> Executor client
            | SchedulerOutput / ModelRunnerOutput
            v
rank-local worker process(es)
  GPUWorker
    -> GPUModelRunner
    -> attention backend / model / sampler / speculator

这里最重要的不是类名,而是状态所有权:

状态唯一或主要 owner为什么不能放到别处
HTTP、chat template、外部模型名serving/API process不应污染 GPU 调度热路径
asyncio waiter、增量 detokenizerOutputProcessor属于客户端语义,不是设备资源状态
waiting/running、token progressScheduler所有 admission 和完成更新必须串行一致
logical request -> KV blocksKVCacheManager与调度准入、抢占和 prefix cache 强耦合
CUDA buffer、model、attention metadatarank-local ModelRunner不能跨进程共享普通 Python 可变状态
SSE socket 与客户端回压API processGPU kernel 已 launch 后不能依赖 socket 是否可写

这条边界解决了在线推理的核心冲突:协议是高并发且可能很慢,GPU 状态必须一致且持续前进。

2. 一个请求在源码中的对象变形

不要把全流程中的 request 当成同一个对象。它至少经过下面这些形态:

阶段对象关键字段或含义
HTTP 入站ChatCompletionRequestmessages、stream、tools、sampling 参数
render 后EngineInputprompt token IDs/embeds、multimodal features
API 到 coreEngineCoreRequestrequest ID、prompt、params、priority、LoRA、trace
core 内部Requeststatus、output IDs、computed count、block hashes、spec IDs
一轮调度SchedulerOutput本轮 request->token 数、KV blocks、spec IDs、encoder inputs
runner 输入InputBatchpacked input IDs、positions、block tables、slot mapping
GPU 输出SamplerOutput / ModelRunnerOutputsampled IDs、rejected count、logprobs、connector output
core 输出EngineCoreOutput(s)单请求新增 token、finish reason、events、stats
API 输出RequestOutput增量/累计 token 与 text、usage、finished
网络输出JSON 或 SSE chunkOpenAI-compatible response

request_id 是这些对象跨进程关联的主键,但不是全部正确性。异步执行时还必须让“这一轮的 SchedulerOutput”与“这一轮返回的 ModelRunnerOutput”配对,否则 token count 和 KV 状态会错位。

3. 完整生命周期总览

以下先给出单请求 happy path。第 10 到 16 步在 decode 中循环;continuous batching 会让其他请求 在任意一次循环进入或退出同一 batch。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
T0  POST /v1/chat/completions
T1  validate + render chat template + tokenize
T2  build SamplingParams and EngineCoreRequest
T3  register local output collector
T4  IPC enqueue to EngineCoreProc
T5  EngineCore converts to internal Request and enters waiting
T6  Scheduler prefix lookup + admission + KV allocation
T7  build SchedulerOutput
T8  Executor dispatches SchedulerOutput to GPU worker(s)
T9  ModelRunner updates resident request state and packs InputBatch
T10 build block table / slot mapping / attention metadata
T11 model forward writes KV and produces hidden states
T12 logits processing + sample or speculative rejection sampling
T13 async D2H; optionally draft proposals for next iteration
T14 Scheduler commits accepted output and repairs optimistic state
T15 EngineCoreOutputs cross IPC; OutputProcessor detokenizes
T16 generator yields RequestOutput; serving emits JSON/SSE
T17 repeat T6-T16 or finish, free ownership, close stream

3.1 生命周期 I/O contract

流程要解决的问题Input核心方法传给下游的 Output主要性能维度
T0-T2 协议/输入messages 和外部参数不能直接调度HTTP JSON、tokenizer、model configrender/tokenize/校验;InputProcessor 统一模型输入EngineCoreRequest + cloned paramsAPI CPU/TTFT;让 core 热路径不处理协议分支
T3-T4 异步提交快结果与多请求乱序不能丢失engine request、request ID先建 collector/parent mapping,再 IPC enqueuecore 收到 DTO;API 持有可等待的 collectorAPI 并发、IPC latency、正确性
T5 core requesttransport DTO 缺少调度状态EngineCoreRequest、block hasher、grammar/MM cache建立 Request、hash/counters/statuswaiting Requestprefix lookup 前置 CPU、queue TTFT
T6-T7 调度/KV动态请求争用 token 和物理 slotswaiting/running state、budgets、cache/connectortoken debt、prefix hit、allocation、preemptionSchedulerOutputbatch 吞吐、TTFT/ITL、公平性、KV capacity
T8-T10 执行准备Python request 不能直接喂 kernelSchedulerOutput、resident state、block tablesrank dispatch、ragged pack、slot/attention metadataInputBatch + forward contexthost launch gap、padding、metadata/H2D
T11-T13 GPU/sample计算本轮状态并产生合法 tokenpacked tensors、weights/KV、sampling/grammar/spec stategraph/eager forward、selective logits、sample、async D2HModelRunnerOutput/future + next draftkernel time、TPOT/ITL、spec acceptance
T14 core commit乐观状态要按真实结果提交/回滚同一 SchedulerOutput + runner resultaccept/reject、token/grammar/finish、freeper-request EngineCoreOutputcommit CPU、KV 回收速度、正确性
T15-T16 输出token delta 不是稳定 OpenAI 文本core outputs、decode/parent state、socket增量 detokenize、聚合、JSON/SSE flush客户端可见 text/tokenperceived TTFT/ITL、API CPU、网络回压
T17 循环/结束继续生成或归还所有 ownershipupdated request/finish/abort重新进入 schedule,或 free KV/collector下一轮状态或最终 response并发 capacity、泄漏/preemption风险

表中 Output 是下游真正消费的 contract。每个模块的字段、状态变化和 fallback 见顶部对应模块文档。

下面逐段展开。

4. T0-T2:HTTP 请求如何变成 EngineCoreRequest

4.1 Router 只做协议边界

Chat router 将 Pydantic 请求交给 OpenAIServingChat.create_chat_completion()。 真正的生命周期入口在 _create_chat_completion()

  1. 选择 tokenizer、chat template 和 tool/reasoning parser。
  2. render_chat_request() 把 messages 变成 conversation 与一个或多个 EngineInput
  3. 创建 chatcmpl-* request ID;批量 prompt 会派生 sub-request ID。
  4. 根据 max_model_len、prompt length 和用户参数约束 max_tokens
  5. request.to_sampling_params() 将 OpenAI 参数归一化为 engine 的 SamplingParams
  6. 调用 engine_client.generate(),也就是 AsyncLLM.generate()

设计思路是“在跨 EngineCore 进程前完成协议归一化”。Scheduler 不理解 messages、role、tool call 或 OpenAI JSON;它只处理已经确定的 token 和生成约束。

4.2 InputProcessor 固化 engine contract

AsyncLLM.add_request() 调用 InputProcessor.process_inputs(), 构造 EngineCoreRequest。这一层处理:

  • text/token IDs/input embeds 的统一输入形式;
  • sampling 或 pooling task;
  • multimodal feature 与 placeholder;
  • LoRA、priority、arrival time、DP rank、trace header;
  • sampling/pooling params clone、generation config/default EOS 合并。

到这里以后,prompt 文本只在输出侧为了 detokenize/展示而保留;core 计算依赖 token IDs/embeds。 InputProcessor 返回后,AsyncLLM.add_request() 才分配内部唯一 request ID;n > 1 时也在这里建立 ParentRequest 并 fan-out child EngineCoreRequest,而不是在 InputProcessor 内展开。

性能结果:render/tokenize 从 GPU owner 进程移出,不会直接延长每轮 GPU launch gap;但超长文本、 慢 tokenizer 或多模态预处理仍会增加请求自身 TTFT,因此 API 侧需要独立 CPU profiling。

5. T3-T5:先注册消费者,再跨 IPC 入队

5.1 为什么 OutputProcessor.add_request() 必须先执行

AsyncLLM._add_request() 的顺序是:

1
2
self.output_processor.add_request(request, ... , queue)
await self.engine_core.add_request_async(request)

这是一个明确的并发正确性设计。小 prompt、小模型或 cache hit 请求可能很快完成;如果先通知 EngineCore,结果可能在 per-request collector 建立前返回,造成 late output 无法路由。

本地 RequestOutputCollector 建立后,EngineCoreClient 才把 EngineCoreRequest 发送给独立 EngineCoreProc。API coroutine 此后只等待自己的 collector,不阻塞共享 engine 输出循环。

5.2 EngineCore 将 transport DTO 变成调度对象

EngineCore.preprocess_add_request() 调用 Request.from_engine_core_request()

  • 建立内部 Request
  • 初始化 status、prompt/output/spec token 列表和 token 计数;
  • 使用 request block hasher 预计算 prefix block hashes;
  • structured output 请求异步初始化 grammar;
  • multimodal feature 从 receiver cache 恢复。

随后 EngineCore.add_request() 调用 Scheduler.add_request(),请求进入 waiting queue。此时它只是 可调度,并未拥有本轮计算所需的全部物理 KV slot。

6. Request 状态机与关键 token 计数

RequestStatus 至少区分 waiting、running、preempted、等待 grammar、等待 remote KV、 finished 等状态。理解调度必须先理解这些量:

字段含义
num_tokens当前已确定序列长度,通常是 prompt + 已提交 output
num_computed_tokenstarget model 已实际计算并可依赖的 token 位置数
spec_token_idsdrafter 提议但尚未被 target 验证的 token
num_tokens_with_specnum_tokens + len(spec_token_ids) 对应的追赶目标
num_output_placeholdersasync scheduling 中为尚未回收结果预留的逻辑位置
num_in_flight_tokens已调度但结果尚未应用的 token 数

普通 decode 可以理解为:新 output token 被提交后,num_tokens 增长,而它对应的 KV 尚未计算, 下一轮就产生一个 token debt。投机解码一次产生多个未验证位置,debt 变大;prefix hit 则让 num_computed_tokens 从 0 直接跳到命中长度附近。

这正是 vLLM Scheduler 能统一各种模式的基础。

7. T6-T7:Scheduler 的统一 token-debt 模型

7.1 源码中没有互斥的 prefill/decode phase

Scheduler.schedule() 的核心注释直接说明:Scheduler 不维护互斥的“prefill phase” 和“decode phase”。它尝试让每个请求的 num_computed_tokens 追上 num_tokens_with_spec

抽象成公式:

1
2
3
4
5
6
7
8
9
10
debt(req) = num_tokens_with_spec
            + num_output_placeholders
            - num_computed_tokens

scheduled(req) = min(
    debt(req),
    per-request limits,
    remaining token budget,
    max_model_len headroom
)

同一个表达覆盖:

  • 首次 prompt:debt 是未计算 prompt 长度;
  • chunked prefill:一次只偿还 debt 的一部分;
  • prefix cache:先提高 computed count,只计算 suffix;
  • 普通 decode:每次通常欠一个 token;
  • speculative decode:把 draft tokens 一起变成待 target 验证的 debt;
  • async scheduling:placeholder 表达已经在 pipeline 中但输出尚未回收的位置。

7.2 每轮调度的实际顺序

可将源码压缩成以下伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
token_budget = max_num_batched_tokens
encoder_budget = max_encoder_tokens

for request in running:
    new_tokens = bounded_debt(request, token_budget)
    new_tokens = fit_encoder_and_model_len(request, new_tokens)
    blocks = kv_manager.allocate_slots(request, new_tokens, lookahead)
    while blocks is None:
        victim = choose_preemption_victim()
        preempt(victim)
        retry allocation
    if allocation succeeded:
        add request/token count/blocks/spec IDs to this iteration
        token_budget -= new_tokens

if no preemption happened:
    for request in waiting order:
        reject or skip blocked grammar/remote-KV/LoRA cases
        cached_blocks, cached_tokens = prefix_lookup(request)
        external_tokens = connector_lookup_if_enabled(request)
        new_tokens = bounded_remaining_sequence(request)
        blocks = allocate cached + external + new + lookahead
        if not fit: stop or skip according to reason
        move request to running and add it to this iteration

build SchedulerOutput
optimistically advance num_computed_tokens by scheduled count

“先 running 后 waiting”保护已在 decode 的请求,减少其 ITL 抖动;waiting 只有在没有刚发生的抢占时 才尝试 admission,避免一边抢占一边立刻塞入更多请求造成抖动。

7.3 SchedulerOutput 是一轮执行的不可变计划

SchedulerOutput 主要携带:

  • scheduled_new_reqsscheduled_resumed_reqsscheduled_running_reqs
  • num_scheduled_tokens: request_id -> count
  • request 对应的新 KV blocks 和 block copy;
  • scheduled_spec_decode_tokens
  • multimodal encoder inputs、structured-output 标志、finished IDs;
  • KV connector metadata。

Executor 和各 rank 必须消费同一逻辑计划。Scheduler 在 _update_after_schedule() 中乐观增加 num_computed_tokens,因为这允许 pipeline 继续组下一轮;如果投机 token 被拒绝,稍后 update_from_output() 必须精确回退。

7.4 抢占为什么是必要但昂贵的

KVCacheManager.allocate_slots() 返回 None 表示物理资源不足。Scheduler 按 FCFS 或 priority 选择 victim,_preempt_request() 会:

  1. 释放该请求 KV block ownership;
  2. 清理 encoder cache;
  3. 状态改为 PREEMPTED
  4. num_computed_tokens 重置为 0,清空 spec token;
  5. 放回 waiting queue,之后尽量通过 prefix cache 复用,否则重算。

最终性能表现:抢占让服务避免直接 OOM,并允许更高优先级请求取得资源;代价是重算、cache 污染和 p99 峰值。持续出现 preemption 不是“GPU 很忙”的好现象,而是 admission/KV 容量配置失败。

8. Scheduler 的设计延伸:公平性、吞吐与尾延迟

调度不是单目标优化。一个实用模型是:

1
2
maximize useful_tokens_per_second
subject to TTFT/ITL SLO, KV capacity, model length, fairness, rank consistency

关键冲突如下:

决策正向结果负向结果
大 token budget大 batch、较高吞吐单轮更长,decode ITL/p99 可能升高
running 优先decode 更平滑waiting TTFT 可能上升
小 prefill chunk限制 head-of-line blockingprompt 分多轮,TTFT 和调度开销可能上升
prefix-hit 优先少算 FLOPs,整体吞吐高miss 请求可能不公平
aggressive admissionGPU 更容易满载KV 紧张、抢占与尾延迟抖动
priority业务 SLO 可表达低优先级饥饿,需要 aging/配额

因此线上调参不能只最大化平均 tokens/s。正确做法是给定 workload 分布和 SLO,寻找最高 goodput。

9. T6:Paged KV Cache、Prefix Cache 与物理内存

9.1 两层结构

KVCacheManager 是 Scheduler 的逻辑接口;BlockPool 管理物理 KVCacheBlock、free queue、hash map 和 ref count。不同 attention 类型由 coordinator/manager 适配,包括 full attention、sliding window、R-SWA、Mamba、cross attention 等。

逻辑上一个 request 拥有 block table:

1
2
3
logical block 0  -> physical block 317
logical block 1  -> physical block  42
logical block 2  -> physical block 901

attention kernel 通过 block table/slot mapping 找到物理 KV,不要求整条序列连续分配。

9.2 get_computed_blocks() 做什么

prefix cache key 基于 token block hash 以及影响模型状态的额外信息。普通单 full-attention group 中, hash 粒度等于物理 block size,因此只命中完整、已经计算的 cache blocks。当前源码在多 KV group 且 prefix_match_unit 小于某个物理 group block size 时还支持 fine-grained lookup;命中可落在物理 block 内部,该 shared tail 会在继续写入前转成 private CoW block。几个关键边界:

  • prefix caching 关闭、prompt logprobs 等不允许读 cache 的请求直接 miss;
  • 普通整 block 模式下,page/block 不完整尾部不能命中;fine-grained 模式需要单 block CoW copy;
  • 即使 prompt 全命中,也要重新计算最后一个 token 才能得到下一 token logits;
  • hybrid cache group 必须协调不同 layout 可共同复用的最长前缀;
  • remote KV connector 的命中与本地命中分别记账。

若 prompt 长度为 P,本地/远端可安全命中 H 个 token,则 target prefill 的主要 token 计算量从 P 降到约 P-H,但要计入 block 对齐与最后 token 重算,不能简单声称等于零。

9.3 allocate_slots() 的完整账本

源码把 request token 区间分成:

1
| computed | new local cached | external cached | new compute | lookahead |

分配过程同时处理:

  1. 已不再需要的 sliding-window block;
  2. 本地命中 block 的 ref count;
  3. remote KV 即将加载的 slot;
  4. 本轮要写入的 new token slot;
  5. EAGLE 等 KV-aware drafter 的 lookahead slot;
  6. fine-grained partial hit 的 private block 与 pending CoW copy;
  7. watermark、full-sequence-fit 和 in-flight reservation。

投机 token 可能写入临时 KV,但只有已经验证、finalized 的 token 能进入可复用 prefix cache。否则另一 请求可能读取错误分支,造成静默语义错误。

9.4 Paged KV 最终解决了什么

它并不改变 attention 数学复杂度。它的主要结果是:

  • 不为每个请求预留连续 max_model_len
  • 降低外部碎片和 over-reservation;
  • request 可动态增长,完成后 block 可细粒度归还;
  • 相同 full block 可通过 ref count 被多个请求共享;
  • 同样显存通常能容纳更多有效 KV token,从而提高可并发序列数和吞吐。

代价是内部 page 尾部碎片、block table 寻址、hash/ref-count CPU 开销,以及 page size 对 kernel/cache locality 的影响。最佳 block size 依赖模型 KV bytes/token、平均序列长度和 attention backend。

10. T8-T13:从 SchedulerOutput 到一次 GPU forward

10.1 EngineCore 与 Executor 边界

EngineCore.step() 的顺序是:

1
2
3
4
5
6
scheduler.schedule()
model_executor.execute_model(..., non_block=True)
scheduler.get_grammar_bitmask()
future.result() / sample_tokens()
process aborts that arrived while GPU was running
scheduler.update_from_output()

non_block=True 允许 CPU 准备 grammar 或其他工作。batch queue/async scheduling 路径还能让多轮执行 进入 pipeline,但每个 SchedulerOutput 必须与自己的 future 一起保存。

10.2 ModelRunner 的 resident state

V2 GPUModelRunner.execute_model() 先把动态调度变化应用到常驻状态:

  • finish/free 已结束 request slot;
  • add new/resumed request;
  • update running request 和 block table;
  • 应用 staged KV block writes/copies。

随后 prepare_inputs() 将 ragged request 打包为 InputBatch

  • request ID 排序与 idx_mapping
  • 每请求 scheduled token count;
  • packed input_idspositions
  • query_start_loc 前缀和;
  • prefill/decode 判定;
  • spec draft token 展开;
  • 需要计算 logits 的位置索引。

这是“动态 CPU 对象”到“稳定 GPU buffer”的关键桥梁。

10.3 attention metadata

prepare_attn() 从 block table 构造:

  • per-cache-group block tables;
  • 每个 query token 的 slot mapping;
  • sequence length、query start、causal mask 等 backend metadata。

模型层不需要理解 Scheduler,只通过 forward context 中的 attention metadata 读写正确 KV slot。

10.4 eager、piecewise graph 与 full graph

runner 根据 batch descriptor 选择:

路径方法适合场景主要成本
eagerself.model(**model_inputs)动态/调试/不支持 graphPython 与 driver launch overhead
piecewiserun_pw_graph()部分 shape 可编译/捕获graph break 与多套 capture 管理
full graphrun_fullgraph()稳定 decode shapepadding、capture 显存、fallback

CUDA Graph 的效果是消除一部分 host launch 开销,不会减少 Transformer 的 FLOPs 或 HBM 读写。 小 batch decode 中 host gap 占比高时收益最明显;大 prefill 已由大 kernel 主导时相对收益较小。

10.5 forward、logits 与 sample

模型 forward 写入本轮 KV,并返回 hidden states。最后一个 PP rank:

  1. 只 gather 需要 logits 的 hidden positions;
  2. compute_logits()
  3. structured output grammar bitmask 先约束 logits;
  4. 普通路径进入 sampler;有 draft token 时进入 rejection sampler;
  5. AsyncOutput 在 copy stream 发起 D2H,尽量与后续 GPU 工作重叠。

PP 非末 rank 返回 IntermediateTensors,最终采样结果再广播给其他 rank,使各 rank 的 request/KV 状态保持一致。

11. T14-T17:提交结果、detokenize、stream 与结束

11.1 Scheduler 只提交被接受的状态

Scheduler.update_from_output() 将 batch result 拆回 request:

  • 降低 num_in_flight_tokens
  • 读取本请求 sampled token IDs;
  • 若有 speculative tokens,计算 accepted/rejected 数并回退 rejected 的 computed count;
  • 逐 token append output,检查 EOS、stop、length 与 grammar;
  • 完成时释放 request ownership、encoder state 和 KV blocks;
  • 生成 EngineCoreOutput,附带 events/stats/finish reason。

这里是 Scheduler 乐观状态与真实 GPU 结果的 commit point。

11.2 OutputProcessor 恢复客户端语义

AsyncLLM.output_handler() 批量拉取 EngineCoreOutputs,分 chunk 调用 OutputProcessor.process_outputs(),避免一次处理大量请求长期占用 asyncio loop。

OutputProcessor 负责:

  • internal child ID 到 parent request 的聚合;
  • 增量 detokenization、UTF-8 稳定边界、stop string;
  • logprobs、usage、reasoning/tool parser;
  • RequestOutput 推入对应 collector;
  • stop string 在 API 侧触发时反向 abort core。

AsyncLLM.generate() 消费 collector 并 yield;serving 再构造非流式 JSON 或 SSE chunk。

11.3 回压与取消的真实边界

  • 已经 launch 的 CUDA kernel 通常不能因 HTTP disconnect 中途撤销;abort 阻止后续调度,并在结果回来 后清理。
  • RequestOutputCollector 可合并 delta,减少慢客户端导致的 queue item 爆炸。
  • 生产环境仍应限制每请求缓存 byte/token、socket write timeout 和最大暂停时间。
  • 慢客户端若继续占用 KV,会伤害其他请求;若直接 abort,则牺牲该请求完成率。需要显式策略。

12. 一轮普通 decode 的精确时序

假设 prompt 已完成,当前已提交序列为:

1
[prompt tokens..., y0, y1]

下一轮大致发生:

  1. num_tokens 已包含 y1,但 y1 对应的新 KV 位置需要 target 计算。
  2. Scheduler 发现一个 token debt,分配一个 slot。
  3. runner 输入通常是 y1,position 是当前末尾位置。
  4. attention 读取旧 block table,并把 y1 的 K/V 写入新 slot。
  5. hidden state -> logits -> sampler 得到 y2
  6. Scheduler 提交 y2,下轮再计算 y2 对应的 KV。

这解释了为什么“采样出 token”和“该 token 已写入可依赖 KV”不是同一时刻,也解释了各种 off-by-one、prefix 全命中仍需最后 token forward 的来源。

13. Speculative Decoding:从理论到 vLLM 源码

13.1 它解决什么问题

自回归 decode 每次 target forward 通常只产生一个新 token,尤其小 batch 时受权重读取、kernel launch 和同步延迟限制。投机解码让便宜的 proposer 一次猜 K 个 token,再让 target 在一次更宽的 forward 中并行验证这些位置。如果连续接受多个 token,一次 target iteration 可提交多个输出。

它优化的是“每个 target iteration 的有效 token 数”,不是免费减少所有计算。

13.2 vLLM 支持的 proposer 形态

SpeculativeConfigvllm/v1/spec_decode 包含多类方法:

  • draft model:更小的独立模型;
  • EAGLE/EAGLE3、MTP:利用 target hidden state 或多 token prediction head;
  • Medusa:多 head proposal;
  • n-gram/prompt lookup、suffix decoding:从已有 token 模式提议,几乎没有模型 forward 成本;
  • DFlash、DSpark 等专用 proposer。

不同 proposer 共享“proposal -> target verify -> accept/repair”的 contract,但 draft 成本、KV 需求、 可用模型和接受率完全不同。

13.3 vLLM V2 的跨轮数据流

当前 GPUModelRunner.sample_tokens() 的关键顺序是:

1
2
3
4
5
6
7
8
target sample / verify current iteration
  -> start async D2H copy
  -> postprocess accepted/rejected state
  -> speculator.propose(...) for the next iteration
  -> DraftTokensHandler stores draft IDs
  -> normal scheduling returns draft IDs to Scheduler;
     async scheduling keeps placeholder/worker state in its pipelined path
  -> next SchedulerOutput carries scheduled_spec_decode_tokens

下一轮 prepare_inputs()

  1. 读取每请求 draft token 数;
  2. 将上一已采样 token和 draft tokens 组合成 packed input;
  3. 展开 request mapping 与 logits positions;
  4. target 一次计算 bonus position + draft positions;
  5. RejectionSampler 决定接受前缀和替代 token。

proposal 放在 async D2H 启动之后,可以把 CPU copy 延迟与 drafter GPU 工作部分重叠。

13.4 greedy 验证

对于 greedy decode,沿 draft chain 从左到右:

1
2
3
draft:   d1 d2 d3 d4
target:  d1 d2  x ...
accept:  d1 d2 x

接受连续相等的 d1,d2,第一个不匹配位置提交 target 自己的 x,后续 draft 作废。一次 verify 至少能得到一个 target token,最多得到 K+1 个 token(具体 bonus 定义依实现路径)。

13.5 sampling 时为什么需要 rejection sampling

若 target 分布为 p(x),draft 分布为 q(x),不能只比较采样 token 是否相等,否则会改变最终 分布。经典 acceptance probability 为:

1
accept(d) = min(1, p(d) / q(d))

拒绝后从归一化残差 max(p-q, 0) 采样。vLLM 的 RejectionSampler 在 probabilistic draft 模式 接收 target logits、draft logits、temperature/seeds,并调用 GPU rejection-sampling kernel;这保证 正确配置下输出仍服从 target 分布,而不只是“看起来相近”。

13.6 KV 与计数如何修复

Scheduler 组 batch 时将 spec IDs 加入 scheduled_spec_decode_tokens,并乐观推进 computed count。 GPU 返回后:

1
2
3
num_accepted = max(len(generated_token_ids) - normal_bonus_count, 0)
num_rejected = num_draft_tokens - num_accepted
num_computed_tokens -= num_rejected

同时 worker 的 request state、slot table 和 penalty state按 num_sampled/num_rejected 更新。未接受 token 对应的临时 slot 可以回收,不能 hash 成共享 prefix。EAGLE/Mamba 等 proposer 还需 lookahead block 或 专用 recurrent state,因此 KV 容量规划必须包含 speculative headroom。

13.7 速度模型与最终表现

令:

  • A:每轮平均接受的 draft token 数;
  • L = A + 1:每次 target verify 平均提交 token 数,+1 是 target/bonus token;
  • C_t:普通一次 target decode 成本;
  • C_v(K):target 验证 K 个 draft 的成本;
  • C_d(K):draft/proposal 成本;
  • C_o:tree、sampling、copy、状态修复等开销。

近似 speedup:

1
speedup ~= L * C_t / (C_v(K) + C_d(K) + C_o)

因此源码设计最终应在指标上表现为:target forward 次数约从 N 降到 N/L,TPOT/ITL 下降;但 每次 target forward token 更宽、临时 KV 更多。以下场景通常收益好:

  • target 大且 memory/launch bound,draft 显著便宜;
  • acceptance length 高;
  • batch 较小,普通 decode 没把 GPU 吃满;
  • proposal/verify shape 能走 CUDA Graph。

以下场景可能持平或变慢:

  • 高并发大 batch 已充分利用 target;
  • domain mismatch、temperature 高导致接受率低;
  • draft 模型不够小,或额外 TP 通信昂贵;
  • K 太大,verify/KV/graph padding 成本超过多接受的 token;
  • structured output/LoRA/backend 组合频繁 fallback。

必须同时记录 acceptance rate、mean acceptance length、draft tokens/s、accepted tokens/s、TPOT 和 端到端 goodput,不能只报 acceptance 百分比。

14. 常见优化手段:设计、实现和可观察结果

优化核心思路与源码落点应看到的结果退化条件或代价
Continuous batchingScheduler 每轮重组 running/waiting,而非静态等待整批结束GPU busy、tokens/s、并发能力上升高负载排队增加 TTFT;大 batch 拉高 ITL
Chunked prefill用 token debt 和 threshold 将长 prompt 分轮decode 不再被整段长 prefill 阻塞,ITL/p99 更稳长 prompt 自身 TTFT、调度次数增加
Prefix cachingblock hash + ref count 复用 full computed blocks命中请求 prefill tokens/FLOPs 降低,TTFT 改善低复用时 hash/tree 维护;block 尾部不能全用
Paged KV非连续 block table 动态增长同显存容纳更多有效 KV token,减少外部碎片page 尾部内部碎片、寻址/bookkeeping
CUDA Graph稳定 buffer + full/piecewise replay小 batch decode host gap、TPOT 下降capture 显存、padding、多 shape 与 fallback
torch.compile/fused op合并 Python/op dispatch,生成专用 kernellaunch 数和中间读写减少编译时间、graph break、模型/backend 覆盖
Optimized attentionFlashAttention/FlashInfer 等 backend 使用 paged metadataattention kernel time 和 HBM traffic 降低shape、dtype、GPU 架构决定最优 backend
Quantization权重/KV/activation 降位宽权重带宽与显存下降,可增 batch/KVdequant kernel、精度、硬件支持;小模型未必更快
Async outputcopy stream D2H 与 proposal/下一轮工作重叠GPU/CPU gap 下降tensor 生命周期、stream fence 更复杂
Structured output GPU maskgrammar state生成 mask,采样前应用保证格式且避免无效重试grammar CPU 或 mask 更新成为瓶颈
LoRA batching基座权重共享,runner 按 batch 激活 adapter多租户显存远低于完整模型复制adapter 数限制、动态 shape、额外 gather/kernel
Spec decodedraft 多 token + target 并行 verify高 acceptance 时 TPOT/ITL 下降draft/verify/临时 KV 开销,低接受率退化
KV connector/P-D外部加载/发送 KV,prefill 与 decode 资源解耦混合 workload 隔离,decode SLO 更稳KV 网络传输、路由、失败重算和背压

15. 并行设计:TP、PP、DP 与 P/D 分离

15.1 Tensor Parallel

TP 切分层内权重,rank 间用 all-reduce/reduce-scatter/all-gather 保持数学等价。它解决单卡放不下模型 或单卡算力不足;代价是每层通信。decode 小 batch 下计算量少,collective latency 可能吞掉扩卡收益。

15.2 Pipeline Parallel

PP 按层切 stage,传递 activation。vLLM 用 intermediate tensors 和 sampled-token broadcast 保持各 stage 状态一致。它降低单 rank 权重占用,但引入 pipeline bubble;需要 batch queue/microbatch 才能 提高利用率,单请求延迟通常不会随 stage 数线性下降。

15.3 Data Parallel 与 expert parallel

DP 复制模型并路由请求,天然扩展独立请求吞吐。MoE 场景还可用 EP/EPLB 分布 expert;最终性能取决于 token routing、expert imbalance 和 all-to-all,而不仅是 GPU utilization。

15.4 Prefill/Decode disaggregation

KVConnector 让 Scheduler 同时核算 local cached、external matched 和 in-flight KV。 P 节点计算 prompt KV,D 节点加载后继续 decode。它解决两种阶段资源特征不同的问题:prefill 更偏 compute,decode 更偏权重/KV 带宽与低延迟。

收益条件:长 prompt、足够高的 KV 传输带宽、稳定路由和可观的阶段干扰。短 prompt 下,序列化、网络 和 bootstrap 固定成本可能大于隔离收益。remote load 失败时源码会标 invalid block 并重算或报错, 这是 correctness 路径的一部分,不是边缘功能。

16. 如何从源码验证每个优化真的工作

16.1 最小实验矩阵

固定模型、硬件、dtype、seed、请求长度分布,再分别运行:

实验AB必看指标
chunked prefilloff多个 chunk size长 prompt TTFT、并发 decode ITL/p99、tokens/s
prefix cache冷且随机高共享 prefixcached tokens、prefill GPU time、TTFT、KV usage
CUDA Grapheagergraph/compilehost gap、kernel 数、TPOT、graph hit/fallback
speculativeoff不同 K/方法mean accept length、draft/accepted rate、TPOT、显存
quantizationBF16目标量化精度、权重显存、tokens/s、kernel time
TP1 GPU2/4 GPUcompute time、NCCL time、端到端 goodput
P/DcolocateddisaggregatedP queue、D ITL、KV transfer time/failure、总成本

16.2 用 trace 对齐因果

启用 VLLM_FLOW_TRACE=1 后,按 request_id 追踪 Step 8-17;再结合 profiler/Nsight:

  • Step 11 到 13:调度完成到 runner forward;
  • Step 13 到 14:model 与 sampler;
  • Step 14 到 15:GPU output 到 Scheduler commit;
  • Step 15 到 17:output IPC、detokenize 与 HTTP。

若 GPU kernel 快但 ITL 仍高,问题可能在 schedule/IPC/output;若 cache hit 高但 TTFT 不降,需要检查 block 对齐、最后 token 重算、排队或 remote load;若 spec acceptance 高但 TPOT 不降,应拆出 draft、 verify、graph fallback 和临时 KV 成本。

16.3 判断“性能结果达到”的标准

一个优化只有同时满足以下条件才算成功:

  1. 目标指标改善,例如 tokens/s 或 p99 ITL;
  2. 非目标指标没有越过 SLO,例如 TTFT 没因过度 batching 失控;
  3. 结果在 warmup 后、多个并发档位和足够样本上稳定;
  4. correctness 与输出分布不变,量化近似除外但需单独评估;
  5. GPU 显存、CPU、网络和失败恢复成本都计入。

17. 关键正确性不变量清单

  1. collector 必须先于 core enqueue 注册。
  2. request 只有在 KV allocation 成功后才能出现在本轮执行计划。
  3. SchedulerOutput 与返回 future 必须一一对应。
  4. num_computed_tokens 的乐观推进必须在 reject/failure 时回退。
  5. 未验证 draft token 不能提交到共享 prefix cache。
  6. block ref count 非零时不能物理淘汰。
  7. full prompt cache hit 仍必须保留产生 logits 的计算边界。
  8. PP/TP rank 必须看到一致 sampled token 和 finish 状态。
  9. abort 不能假设已 launch kernel 被取消;late output 必须可安全丢弃。
  10. async D2H/graph/shared buffer 的 tensor 生命周期必须覆盖消费 stream。

18. 推荐的源码阅读顺序

  1. FLOW_TRACE.md:用真实日志建立 17 步地图。
  2. OpenAIServingChatAsyncLLM:看协议到 engine 的边界。
  3. EngineCoreRequest:看进程和状态机。
  4. Scheduler.schedule():围绕 token debt 读 running、waiting、preemption。
  5. KVCacheManagerBlockPool:看 prefix、allocation、ref count。
  6. V2 ModelRunner:从 SchedulerOutput 跟到 input、attention、forward、sample。
  7. RejectionSampler 和各 proposer:读 speculative commit/rollback。
  8. OutputProcessor:闭合 detokenize、stop、abort 和 stream 生命周期。
This post is licensed under CC BY 4.0 by the author.

vLLM / SGLang 面试源码深挖:优化机制与实验

SGLang 请求处理全流程:从 Tokenizer 到 Scheduler、Radix 与输出