NOTE 本文基于 vLLM v0.27.1(tag 6e448d0,commit 6e448d0ea9bf3d88d898b65449ca6dc2aec170ac,2026-08-11)源码剖析。文中文件路径、类名和函数名均以该版本为准;vLLM 迭代很快,阅读时请以你手上的版本对照。

前两篇分别定义了问题(LLM Serving 为什么难)和尺子(用什么指标衡量)。在深入调度、KV Cache、GPU 执行等单个战场之前,需要先有一张全景图:一个请求进入 vLLM 之后,会经过哪些模块,以什么形式被传递,最后如何变成 token 返回给用户。

本篇的核心问题是:

一个请求如何穿过 vLLM 的整个推理系统——哪个模块负责决策、哪个模块负责执行,它们之间传递的到底是什么?1

一、总览:静态拓扑、进程视图、动态生命周期与数据流

1. 四个视角看同一个系统

我用四个视角看 vLLM V1。静态拓扑说明模块分工;进程视图说明模块在哪个进程、怎样交互;请求生命周期沿时间轴追踪一次请求;数据流追踪文本、token ids、调度元数据和 GPU 张量。

这样可以分清谁保存请求状态、谁决定这一步跑什么,以及 token 和调度结果怎样跨进程移动。

2. 本文的章节安排

本文的章节安排
章 主题 内容
二 静态系统拓扑 自顶向下:API Server 与 AsyncLLM、EngineCore、Scheduler 与 KVCacheManager、Worker 与 Executor
三 进程视图 API Server、EngineCoreProc、WorkerProc 的职责与 IPC
四 一次请求的完整生命周期 从 HTTP 请求到 SSE [DONE] 的时序图
五 数据流 Token 如何穿过整个 Serving 栈
六 本文小结 模块分工,以及与第一篇“四问”的对应
七 自测 7 道题

二、静态系统拓扑(自顶向下)

vLLM V1 的整体架构遵循控制面/数据面分离的经典设计哲学。我们自顶向下,逐层解剖其系统拓扑。

%% 图:vLLM V1 的静态系统拓扑:API 层 → 引擎层 → 调度层 → 执行层,控制面与数据面分离
graph TB
    subgraph "API Layer (控制面入口)"
        Client[Client / OpenAI SDK]
        API[API Server<br/>FastAPI + uvicorn]
    end

    subgraph "Engine Layer (异步引擎)"
        AsyncLLM[AsyncLLM<br/>异步请求管理]
        EngineCore[EngineCore<br/>核心调度循环]
    end

    subgraph "Scheduling Layer (调度层)"
        Scheduler[Scheduler<br/>请求调度 + 资源管理]
        KVCacheManager[KVCacheManager<br/>显存块管理]
        BlockPool[BlockPool<br/>物理块池]
        SOManager[StructuredOutputManager<br/>约束输出]
    end

    subgraph "Execution Layer (执行层)"
        Executor[Executor<br/>分布式执行抽象]
        Worker0[GPU Worker 0]
        Worker1[GPU Worker 1]
        WorkerN[GPU Worker N]
    end

    subgraph "Model Layer (模型层)"
        MR0[ModelRunner<br/>模型前向 + 采样]
        Attn[Attention Backend<br/>FlashAttn / FlashInfer]
        KVCache[GPU KV Cache<br/>物理显存]
    end

    Client -->|HTTP/gRPC| API
    API -->|add_request| AsyncLLM
    AsyncLLM -->|EngineCoreRequest| EngineCore
    EngineCore --> Scheduler
    Scheduler --> KVCacheManager
    KVCacheManager --> BlockPool
    EngineCore --> SOManager
    EngineCore -->|SchedulerOutput| Executor
    Executor --> Worker0
    Executor --> Worker1
    Executor --> WorkerN
    Worker0 --> MR0
    MR0 --> Attn
    Attn --> KVCache
    MR0 -->|ModelRunnerOutput| EngineCore

从这张图看,从 Client 到 EngineCore 是请求入队路径,传递的是”生成什么”;EngineCore 到 Worker 再到 ModelRunner 是执行路径,携带的是这一轮调度出来的 batch 和 block 布局;而 ModelRunner 返回的只是采样结果,不是下一次 forward 的完整状态。调度状态留在 EngineCore,模型侧负责的是单次前向计算。

1. API Server 与 AsyncLLM:异步并发处理的桥头堡

入口位于 vllm/entrypoints/openai/api_server.py,基于 FastAPI 构建的 HTTP 服务器,实现了 OpenAI 兼容的 /v1/chat/completions、/v1/completions 等 REST API。

AsyncLLM(vllm/v1/engine/async_llm.py)是面向外部的异步引擎接口。它自己不做重活,而是把工作交给三个同目录下的协作者:

  • InputProcessor(input_processor.py):把文本 / 多模态输入 tokenize、校验采样参数,产出 EngineCoreRequest;
  • EngineCoreClient(core_client.py):与 EngineCore 通信的客户端。默认部署下 EngineCore 运行在独立进程中,两者通过 ZMQ + msgspec 收发 EngineCoreRequest / EngineCoreOutputs(AsyncMPClient);单进程调试时用 InprocClient。这条进程边界是 vLLM V1 让 HTTP 处理、tokenize / detokenize 与 GPU 调度循环互不阻塞的关键;
  • OutputProcessor(output_processor.py):接收 EngineCoreOutputs,做 detokenize、stop 条件检查、logprobs 整理,产出 RequestOutput 交给 API 层流式返回。

因此 AsyncLLM 的职责是:管理请求的异步生命周期、串起上述三者、支持 SSE 流式响应,以及数据并行(DP)场景下的多引擎协调。

2. EngineCore:推理系统的中央调度大脑

EngineCore(vllm/v1/engine/core.py)是 vLLM V1 引擎的核心,它的 step() 方法驱动整个推理循环:

# vllm/v1/engine/core.py (简化)
class EngineCore:
    """Inner loop of vLLM's Engine."""

    def __init__(self, vllm_config, executor_class, ...):
        # 1. 初始化模型执行器
        self.model_executor = executor_class(vllm_config)

        # 2. 初始化 KV Cache
        kv_cache_config = self._initialize_kv_caches(vllm_config)

        # 3. 初始化调度器
        Scheduler = vllm_config.scheduler_config.get_scheduler_cls()
        self.scheduler = Scheduler(
            vllm_config=vllm_config,
            kv_cache_config=kv_cache_config,
            ...
        )

3. Scheduler 与 KVCacheManager:资源管理双核

Scheduler(vllm/v1/core/sched/scheduler.py,约 3000 行)是调度的大脑,决定每一轮迭代中哪些请求参与推理、分配多少 token 预算。

KVCacheManager(vllm/v1/core/kv_cache_manager.py)管理 GPU 显存上的 KV Cache 物理块——分配、释放、前缀缓存复用、驱逐。

4. Worker 与 Model Executor:模型执行的设备抽象

%% 图:Executor 与 Worker:一次 execute_model() 分发给多个 GPU Worker,各自的 ModelRunner 持有分片权重与 KV Cache
graph TB
    EX["Executor<br/><i>执行抽象:单设备或多设备模型调用</i>"]
    EX --> W0 & W1 & WN
    subgraph W0["Worker 0 (GPU 0)"]
        M0["ModelRunner"] --- S0["Model Weights(分片)<br/>KV Cache"]
    end
    subgraph W1["Worker 1 (GPU 1)"]
        M1["ModelRunner"] --- S1["Model Weights(分片)<br/>KV Cache"]
    end
    subgraph WN["Worker N (GPU N)"]
        MN["ModelRunner"] --- SN["Model Weights(分片)<br/>KV Cache"]
    end
    S0 <-.->|NCCL| S1
    S1 <-.->|NCCL| SN

常见 GPU 部署中,Executor 会把一次 execute_model() 调用分发给一个或多个 Worker;Worker 再调用本地的 ModelRunner 执行模型前向、采样和 KV Cache 读写。单卡、同机多进程、Ray 分布式和 external launcher 的进程/设备映射并不完全相同,因此不应把“一个 GPU 固定绑定一个 Worker 进程”写成绝对规则。

每个 Worker 通常负责:

  • GPUModelRunner(vllm/v1/worker/gpu_model_runner.py):负责输入准备(_prepare_inputs())、模型前向(execute_model())、采样(sample_tokens())。本系列图中简写为 ModelRunner。v0.27.1 另有一份新实现 vllm/v1/worker/gpu/model_runner.py,通过 VLLM_USE_V2_MODEL_RUNNER=1 启用,文中以默认实现为主线
  • 模型权重的分片(Tensor Parallel 下每卡持有一部分)
  • KV Cache 物理显存

Executor(vllm/v1/executor/abstract.py)是模型执行抽象层,负责在一个设备或多个设备上执行模型,而不是 Scheduler 本身。v0.27.1 中可见的主要实现包括:

  • UniProcExecutor:单进程单卡
  • MultiprocExecutor:多进程多卡(同机)
  • RayDistributedExecutor:Ray 分布式(直接继承 Executor)
  • RayExecutorV2:Ray 分布式的新实现,注意它继承的是 MultiprocExecutor 而非 Executor——即复用同一套 worker 进程管理逻辑,只把进程的拉起方式换成 Ray(RayWorkerProc(WorkerProc))
  • ExecutorWithExternalLauncher:外部 launcher 场景(继承 UniProcExecutor)

这条继承链本身就说明了一件事:“用不用 Ray”是部署方式的差异,不是执行模型的差异。 Executor 这层抽象的价值就在于把”进程怎么起、卡怎么分”和”一轮 batch 怎么执行”彻底分开,所以 V2 才能靠换掉进程拉起方式来复用同机多进程的全部逻辑。

三、进程视图:谁在什么进程里,如何交互

静态拓扑按模块划分。默认的在线 vllm serve + AsyncMPClient + MultiprocExecutor 部署里,这些模块落在三类进程中。API Server 处理协议和文本;EngineCoreProc 管请求状态和调度;每个 WorkerProc rank 执行模型。

vLLM V1 默认多进程部署:API Server、EngineCoreProc 与 WorkerProc 的线程和 IPC

1. API Server:处理协议与文本

API Server 用 FastAPI 提供 OpenAI 兼容接口,通常由 uvicorn/uvloop 运行。AsyncLLM 在这个进程中创建 InputProcessor、OutputProcessor 和 AsyncMPClient(vllm/entrypoints/openai/api_server.py、vllm/v1/engine/async_llm.py)。

InputProcessor 把输入整理成 EngineCoreRequest;OutputProcessor 接收 EngineCoreOutputs,增量 detokenize 并整理成 RequestOutput。异步输出处理由 asyncio task 驱动。默认多进程路径下,这个进程不持有模型权重,也不运行 GPU forward;调度循环在 EngineCoreProc。

2. EngineCoreProc:持有调度状态

EngineCoreProc 是运行 EngineCore 的独立进程。它初始化 Executor、GPU KV cache 配置和 Scheduler;Scheduler 内部持有 KVCacheManager,负责请求状态、块分配与释放(vllm/v1/engine/core.py、vllm/v1/core/sched/scheduler.py)。

主线程运行 run_busy_loop()。每轮先由 Scheduler.schedule() 生成 SchedulerOutput,再调用 Executor.execute_model(),最后用 Scheduler.update_from_output() 更新请求。EngineCore 保存请求的权威状态,但不运行 HTTP、tokenizer 或 detokenizer;默认多进程执行器下,实际的模型 forward 和 GPU KV 张量在 Worker 侧。

输入、输出 socket I/O 与主循环分开。输入线程 process_input_sockets() 用 MsgpackDecoder 解出请求,放进内部 queue;输出线程 process_output_sockets() 从 queue 取 EngineCoreOutputs,用 MsgpackEncoder 序列化后发送。主线程因此不需要在调度步骤里做 socket 序列化(vllm/v1/engine/core.py)。

3. WorkerProc:一个模型并行 rank

MultiprocExecutor 为每个模型并行 rank 启动 WorkerProc。普通 TP×PP 部署的 rank 数是 TP 与 PP 的乘积;启用其他并行维度时以配置里的 world_size 为准。每个进程初始化一个 Worker;GPU Worker 再创建 GPUModelRunner,加载本 rank 的权重分片,并在 GPU 上读写 KV cache(vllm/v1/executor/multiproc_executor.py、vllm/v1/worker/gpu_worker.py、vllm/v1/worker/gpu_model_runner.py)。

WorkerProc.worker_busy_loop() 在进程主线程等待执行命令,再调用 Worker 的方法。它不处理 HTTP,也不拥有 Scheduler 中的权威 Request 状态。TP 的 all-reduce / all-gather 和 PP 的 rank 间传递发生在 Worker 进程之间,由 PyTorch distributed / device communicator 执行;GPU 上使用 NCCL-backed collective 时,激活也不经过 EngineCore(vllm/distributed/communication_op.py、vllm/distributed/parallel_state.py、vllm/distributed/device_communicators/pynccl.py、vllm/v1/worker/gpu_worker.py)。

4. 两道边界上传什么

API Server 到 EngineCore 的控制通道使用 ZMQ 和 msgpack。AsyncMPClient 发送 ADD(EngineCoreRequest)、ABORT 和 UTILITY 请求;EngineCore 的输入线程解码后放入内部 queue。返回方向由输出线程发送 EngineCoreOutputs。这个通道传请求、token ids 和元数据,不传模型 forward 的激活张量(vllm/v1/engine/core_client.py、vllm/v1/engine/core.py)。

EngineCore 到 Worker 的执行控制由 MessageQueue 广播 SchedulerOutput。单机 MultiprocExecutor 使用共享内存队列;多机时会使用远端队列传输。Worker 通过 response queue 返回 ModelRunnerOutput;execute_model() 默认只等待 output_rank 的结果,其他 rank 不重复回传。Worker 之间的 TP/PP collective 则走各自的分布式通信组,不经过 EngineCore(vllm/v1/executor/multiproc_executor.py、vllm/distributed/device_communicators/shm_broadcast.py)。

5. 启动顺序与就绪握手

单 API Server 时,vllm serve 在当前进程运行 run_server();AsyncLLM 创建 AsyncMPClient,后者绑定 ZMQ sockets 并通过 launch_core_engines() 启动 EngineCore 进程。--api-server-count > 1 时,run_multi_api_server() 先启动 EngineCore,再由 APIServerProcessManager 启动 API Server 子进程。每个 EngineCore 初始化自己的 Executor,MultiprocExecutor 再启动 Worker 进程。

EngineCore 先发 HELLO 注册,启动器返回地址和配置;初始化完成后,EngineCore 报告 READY。API client 还会等 EngineCore 在输入 socket 上发送 EngineCoreReadyResponse。Worker 初始化完成后通过 ready pipe 报告 READY 并交回 response queue 的 handle;Executor 随后等待各队列的 ready barrier(vllm/entrypoints/cli/serve.py、vllm/entrypoints/openai/api_server.py、vllm/v1/engine/async_llm.py、vllm/v1/engine/core_client.py、vllm/v1/engine/utils.py、vllm/v1/engine/core.py、vllm/v1/executor/multiproc_executor.py、vllm/v1/utils.py)。

6. 会改变进程图的部署方式

vLLM V1 的多 API Server 与 data parallel 部署变体

  • UniProcExecutor 在 EngineCore 进程内直接初始化并调用 Worker,不创建 WorkerProc 子进程。InprocClient 是另一层选择:它把 EngineCore 本身也放在调用方进程里。二者改变的是不同的进程边界(vllm/v1/executor/uniproc_executor.py、vllm/v1/engine/core_client.py)。
  • data parallel 会启动多个 EngineCore rank,各自有自己的 Scheduler 和 Executor。在线且配置需要协调时,rank 0 还会启动 DPCoordinator;它汇总队列统计、向 API frontends 发布负载信息,并在需要时协调 MoE request wave。它不是每个 token 的执行通道(vllm/v1/engine/utils.py、vllm/v1/engine/coordinator.py、vllm/v1/engine/core.py)。
  • --api-server-count > 1 会启动多个 API Server 进程,并为它们分配各自的 ZMQ 地址;它增加的是前端进程数,不等于增加模型并行 rank。未显式指定时,参数默认值会根据 DP 配置决定(vllm/entrypoints/cli/serve.py、vllm/entrypoints/openai/cli_args.py)。
  • Ray 改变 Worker 的启动和放置方式。RayExecutorV2 用 Ray actor 承载 RayWorkerProc,并复用 MessageQueue 控制面与 Worker 间的 collective;旧 RayDistributedExecutor 是另一套 Ray executor 实现(vllm/v1/executor/ray_executor_v2.py、vllm/v1/executor/ray_executor.py)。

7. 一个 decode step 怎样穿过三个进程

vLLM V1 一个 decode step 在 API Server、EngineCoreProc 与 WorkerProc 间的时序

EngineCore 收到请求后,主线程调度并广播这一轮的 SchedulerOutput。Worker 执行模型并由 output_rank 返回结果;EngineCore 更新请求,再把 EngineCoreOutputs 交给输出线程。API Server 收到 token ids 后做 detokenize,最后由 HTTP handler 写成 SSE。图里只展开一个 decode step;prefill 也走同一组进程边界。

四、一次请求的完整生命周期

%% 图:一次请求的完整生命周期:从 POST /v1/chat/completions 经 AsyncLLM、EngineCore、Scheduler、Executor 到 GPU,再流式返回
sequenceDiagram
    participant C as Client
    participant API as API Server
    participant E as AsyncLLM
    participant EC as EngineCore
    participant S as Scheduler
    participant KV as KVCacheManager
    participant EX as Executor
    participant W as GPU Worker
    participant MR as ModelRunner
    participant GPU as GPU

    C->>API: POST /v1/chat/completions
    API->>API: 参数解析 & 校验
    API->>E: add_request(prompt, params)
    E->>E: InputProcessor: tokenize → EngineCoreRequest
    E->>EC: EngineCoreClient 经 ZMQ 发送(跨进程)
    EC->>S: add_request(Request)

    Note over S: Request.status = WAITING

    rect rgb(230, 245, 255)
        Note over EC,GPU: === 调度循环 step() ===
        S->>S: schedule() — 选择可运行请求
        S->>KV: get_computed_blocks() — 查 Prefix Cache
        KV-->>S: 缓存命中块 + 未命中数
        S->>KV: allocate_slots() — 分配新块
        KV-->>S: KVCacheBlocks
        Note over S: Request.status = RUNNING
        S-->>EC: SchedulerOutput
    end

    EC->>EX: execute_model(SchedulerOutput)
    EX->>W: forward pass

    rect rgb(255, 245, 230)
        Note over W,GPU: === Prefill 阶段 ===
        W->>MR: execute_model(SchedulerOutput)
        MR->>MR: _prepare_inputs(): input_ids / positions / slot_mapping
        MR->>GPU: 模型 Forward (所有 prompt tokens)
        GPU->>GPU: Attention 计算 + KV Cache 写入
        GPU->>GPU: MLP 计算
        GPU-->>MR: logits
        W->>MR: sample_tokens()
        MR->>MR: Sampling → 第 1 个 output token
    end

    MR-->>EC: ModelRunnerOutput
    EC->>S: update_from_output()
    EC-->>E: EngineCoreOutputs
    E-->>API: 第 1 个 token (TTFT)
    API-->>C: SSE: data: {"token": "Hello"}

    loop Decode 循环 (每步 1 token)
        rect rgb(245, 255, 230)
            S->>S: schedule()
            S->>KV: allocate_slots(1 new token)
            EC->>EX: execute_model()
            W->>MR: execute_model() → _prepare_inputs(1 token per request)
            MR->>GPU: Forward (读历史 KV Cache + 计算新 token)
            GPU-->>MR: logits
            MR->>MR: Sampling → next token
            MR-->>EC: ModelRunnerOutput
            EC->>S: update_from_output()
        end
        EC-->>E: EngineCoreOutputs
        E->>E: OutputProcessor: detokenize + stop 检查
        E-->>API: RequestOutput (stream)
        API-->>C: SSE: data: {"token": "..."}
    end

    Note over S: 遇到 stop token / max_tokens
    Note over S: Request.status = FINISHED_STOPPED
    S->>KV: free(request) — 释放所有块
    API-->>C: SSE: data: [DONE]

1. 流式输出:一个 token 从 GPU 到客户端

上面的时序图把 EngineCore 和 AsyncLLM 之间画成了同步的请求-应答,实际上它们跨进程、各自有独立的循环。下面把 decode 循环中一个 token 的回程放大,重点看两件事:detokenize 发生在哪个进程,以及引擎循环为什么不需要等它。

%% 图:一个 token 的回程:ModelRunner 采样后 D2H,EngineCore 主线程立刻回到 schedule(),序列化与 detokenize 分别在输出线程与进程 A 完成
sequenceDiagram
    participant MR as ModelRunner<br/>(Worker 进程, GPU)
    participant EC as EngineCore 主线程<br/>(进程 B)
    participant OT as EngineCore 输出线程<br/>(进程 B)
    participant OH as AsyncLLM output_handler<br/>(进程 A)
    participant GEN as generate() → SSE<br/>(进程 A)

    Note over MR: 第 N 步 forward 结束, logits 已在 GPU
    MR->>MR: _sample(): sampled_token_ids (GPU tensor)
    MR->>MR: _bookkeeping_sync(): D2H 拷贝 → Python 嵌套 list
    MR-->>EC: ModelRunnerOutput (仅 token ids, 经 worker_response_mq)
    EC->>EC: Scheduler.update_from_output(): 追加 output_token_ids, 判 max_tokens / stop_token_ids
    EC->>OT: output_queue.put(EngineCoreOutputs)
    par 进程 B 继续下一步
        EC->>EC: schedule() 第 N+1 步, 下发 execute_model
        MR->>MR: 第 N+1 步 forward 已在 GPU 上运行
    and 进程 A 处理第 N 步的 token
        OT->>OH: ZMQ 发送 msgspec 序列化的 EngineCoreOutputs
        OH->>OH: OutputProcessor.process_outputs(): IncrementalDetokenizer.update() → 文本增量
        OH->>OH: 检查 stop 字符串, 命中则 abort_requests_async 通知进程 B
        OH->>GEN: RequestOutputCollector.put(RequestOutput)
        GEN->>GEN: await collector.get(), 序列化为 SSE chunk 写回客户端
    end
    Note over OH,GEN: 若消费端慢于生产端, Collector 会把多个 delta 合并成一个 RequestOutput

这张图解释了第三章的进程边界怎样影响流式输出:进程 B 的主线程把 EngineCoreOutputs 放进 output_queue 后立刻回到 schedule(),序列化由输出线程做,detokenize 和 stop 字符串检查由进程 A 的 output_handler 协程做,三者互不等待。代价是 stop 字符串(而非 stop token id)的判定要晚一步——进程 A 检测到后反向发 abort,GPU 可能已经为这个请求多算了一步。另外,RequestOutputCollector 在 DELTA 模式下会把积压的输出合并,因此客户端收到的一个 SSE chunk 不一定恰好对应一个 decode 步。

五、数据流:Token 如何穿过整个 Serving 栈

  ┌──────┐     ┌─────────┐     ┌────────┐     ┌───────────┐
  │Client│────▶│API Server│────▶│AsyncLLM│────▶│EngineCore │
  └──────┘     └─────────┘     └────────┘     └─────┬─────┘
  "Hello,       HTTP JSON       add_request     EngineCoreReq
   tell me                      (text)          (token_ids)
   a joke"                         │
                              Tokenizer
                          [15496, 11, 2425,
                           757, 257, 9707]
                                                     │
                                              ┌──────▼──────┐
                                              │  Scheduler   │
                                              │  schedule()  │
                                              └──────┬──────┘
                                              SchedulerOutput
                                              (scheduled_new_reqs[NewRequestData:
                                                 prompt_token_ids, block_ids, ...],
                                               scheduled_cached_reqs[new_block_ids],
                                               num_scheduled_tokens{req_id: n},
                                               finished_req_ids, ...)
                                                     │
                                              ┌──────▼──────┐
                                              │  Executor    │
                                              │  → Worker    │
                                              └──────┬──────┘
                                                     │
                                              ┌──────▼──────┐
                                              │GPUModelRunner│
                                              │_prepare_inputs│
                                              └──────┬──────┘
                                              input_ids, positions,
                                              block_table, slot_mapping
                                                     │
                                              ┌──────▼──────┐
                                              │   GPU       │
                                              │  Forward    │
                                              │  Pass       │
                                              └──────┬──────┘
                                              logits [vocab_size]
                                                     │
                                              ┌──────▼──────┐
                                              │  Sampling   │
                                              │ (top-p/top-k│
                                              │  /temp)     │
                                              └──────┬──────┘
                                              sampled_token_id
                                                     │
                                              ┌──────▼──────┐
                                              │Detokenizer  │
                                              │ → "Sure"    │
                                              └──────┬──────┘
                                                     │
  ┌──────┐     ┌─────────┐                    ┌──────▼──────┐
  │Client│◀────│SSE Stream│◀───────────────────│  Response   │
  └──────┘     └─────────┘                    └─────────────┘
  "Sure, here's a joke..."

上图沿着数据走了一遍,但同一个请求在每一层其实是不同的对象——它们定义在不同文件、活在不同进程、寿命也不同。把这些对象排成一张表,就能看出”谁持有请求的真状态、谁只是一次性的快照”:

一个请求在各层的对象
对象 定义位置 所在进程 生存期 它是什么
HTTP JSON → ChatCompletionRequest vllm/entrypoints/openai/chat_completion/protocol.py A(API Server) 一次 HTTP 连接 文本 prompt + 采样参数(用户视角)
EngineCoreRequest vllm/v1/engine/__init__.py A → B(msgspec 序列化过 ZMQ) 只传一次 扁平的 IPC 载荷:prompt_token_ids、sampling_params、arrival_time… 已 tokenize,无状态
Request vllm/v1/request.py(from_engine_core_request) B(Scheduler 内部) 从入队到 free() 请求状态的唯一权威副本:status 状态机、num_computed_tokens、output_token_ids、block_hashes
NewRequestData / CachedRequestData vllm/v1/core/sched/output.py B → C(共享内存广播) 一个 step SchedulerOutput 里的两种”订单”:首次调度的请求带全量 prompt_token_ids + block_ids;已在 batch 里的只带增量 new_token_ids + new_block_ids
SchedulerOutput vllm/v1/core/sched/output.py B → C 一个 step 上面两者 + num_scheduled_tokens{req_id: n} + finished_req_ids 等:这一轮”谁跑、跑多少、块在哪”
CachedRequestState + InputBatch 行 vllm/v1/worker/gpu_input_batch.py C(Worker 常驻) 从首次调度到 finished_req_ids Worker 侧对 Request 的镜像,靠增量订单保持同步;InputBatch 是按行排列的持久 batch(token_ids_cpu、block_table)
ModelRunnerOutput vllm/v1/outputs.py C → B(worker_response_mq) 一个 step req_ids + sampled_token_ids: list[list[int]] + logprobs:只有采样结果,不含任何状态
EngineCoreOutput(s) vllm/v1/engine/__init__.py B → A(ZMQ) 一个 step 每个请求的 new_token_ids + finish_reason;仍然只有 token ids
RequestOutput vllm/outputs.py A(OutputProcessor 产出) 一个 SSE chunk 第一次出现文本:detokenize 后的增量 text + token_ids

表里有两条规律。第一,状态只在 B 和 C 各有一份:Request 是权威,CachedRequestState 是靠每步增量同步的镜像;所有跨进程的载荷(EngineCoreRequest、SchedulerOutput、ModelRunnerOutput、EngineCoreOutputs)都是无状态的一次性消息,读完即弃,也因此可以随意序列化、走任何 IPC 通道。第二,文本只在进程 A 出现:从 EngineCoreRequest 到 EngineCoreOutputs 全程都是 token ids,进程 B、C 完全不需要 tokenizer。

六、本文小结

读 trace 时,我先分清两种流量:请求、token ids 和 SchedulerOutput 属于控制流;TP/PP 的激活张量在 Worker 间走 collective,不经 EngineCore 转发。

把它和第一篇的四问对齐,就得到全系列的骨架:

四问与承担模块
四问 承担模块
一、这一轮谁执行、执行多少 Scheduler(+ EngineCore 驱动)
二、状态放哪、怎么复用 KVCacheManager / BlockPool
三、怎么算得更快 ModelRunner / Attention Backend / Kernel
四、怎么扩出去 Executor / Worker / 集合通信
📂 本章源码导航

入口与引擎循环

入口与引擎循环的源码导航
想看什么 从哪开始
HTTP 入口、OpenAI 兼容接口 vllm/entrypoints/openai/api_server.py(路由)、vllm/entrypoints/openai/chat_completion/(chat 接口实现)
异步请求生命周期、流式响应 vllm/v1/engine/async_llm.py
tokenize 与请求构造 vllm/v1/engine/input_processor.py → InputProcessor
与 EngineCore 进程通信 vllm/v1/engine/core_client.py → EngineCoreClient / AsyncMPClient
detokenize 与输出整理 vllm/v1/engine/output_processor.py → OutputProcessor
推理主循环(建议从这里入手) vllm/v1/engine/core.py → EngineCore.step()
调度器给执行层的”订单” vllm/v1/core/sched/output.py → SchedulerOutput / NewRequestData / CachedRequestData
执行抽象与各种部署形态 vllm/v1/executor/abstract.py
一轮 batch 在 GPU 上怎么跑 vllm/v1/worker/gpu_model_runner.py → GPUModelRunner.execute_model() / sample_tokens()
进程启动、ZMQ 握手与 DP 协调 vllm/v1/engine/utils.py、vllm/v1/utils.py、vllm/v1/engine/coordinator.py

七、自测

  1. vLLM V1 里 AsyncLLM 与 EngineCore 为什么分成两个进程?中间传什么?

    答案

    把 tokenize / detokenize、HTTP、流式响应这些 CPU 密集且抖动大的工作从引擎循环里摘出去,让 EngineCore.step() 稳定地驱动 GPU;中间经 ZMQ 传 EngineCoreRequest(token id + 采样参数)与 EngineCoreOutputs(新 token id),不传 tensor。

  2. SchedulerOutput 里有什么、没有什么?为什么 ModelRunner 能只凭它构造这一步的输入?

    答案

    有:每个被调度请求的 id、这一轮的新 token 数、已计算 token 数、block table、采样参数变化、被抢占 / 完成的请求;没有:任何 tensor。ModelRunner 维护持久的 InputBatch,凭这些元数据更新位置与 slot mapping、从 KV 块里读,不需要 Scheduler 传数据。

  3. 一个请求的状态机有哪几个主要状态?从 WAITING 到 RUNNING 要过哪些检查?

    答案

    WAITING → RUNNING → FINISHED(或被抢占回 WAITING / PREEMPTED;结构化输出有 WAITING_FOR_STRUCTURED_OUTPUT_GRAMMAR);准入要 KV 块够(KVCacheManager.allocate_slots)、token budget 够、max_num_seqs 未满、(有 LoRA 时)max_loras 未满。

  4. TP=8 时 Executor 把一步分发给 8 个 Worker,谁做采样?logits 在哪?

    答案

    8 个 Worker 各算自己那 1/8 的权重,激活经 all-reduce 汇合;lm_head 的输出(logits)在所有 rank 都完整(或 gather 到 rank 0),采样在 rank 0(driver)做,其他 rank 的结果丢弃——所以采样相关的 CPU 工作只在一个进程上。

  5. 前端 detokenize 为什么放在 AsyncLLM 而不是 EngineCore?流式输出时“一个 token 一次返回”有什么坑?

    答案

    detokenize 是纯 CPU 且要维护每请求的字符串状态,放引擎里会拖慢 step;坑:BPE 的 token 边界与 UTF-8 字符边界不对齐,一个多字节字符可能跨两个 token,IncrementalDetokenizer 要缓存到能安全输出的完整字符再发。

  6. 默认多进程路径里,EngineCoreProc 与 WorkerProc 各自做什么?SchedulerOutput 和 ModelRunnerOutput 分别通过哪种通道传?

    答案

    EngineCoreProc 持有 Scheduler 和请求状态,驱动调度、执行、更新;WorkerProc 持有本 rank 的 Worker、GPUModelRunner、权重分片和 GPU KV cache。单机时 EngineCore 通过共享内存 MessageQueue 广播 SchedulerOutput,output_rank 通过 response queue 回传 ModelRunnerOutput。

  7. TP/PP、DP、--api-server-count > 1 分别改变哪一部分?RayExecutor 又改变什么?

    答案

    TP/PP 增加 Worker rank,并让 Worker 之间做 collective;DP 增加独立的 EngineCore rank,在线配置需要时由 DPCoordinator 汇总负载或协调 wave;--api-server-count 增加 API Server 进程。RayExecutor 改变 Worker 的启动和放置方式;RayExecutorV2 仍复用 MessageQueue 控制面与 Worker 间 collective。

  1. 链路是固定的:入口(api_server 的 OpenAI 兼容路由)→ AsyncLLM → InputProcessor(tokenize、构造 EngineCoreRequest)→ 经 EngineCoreClient 跨进程送进 EngineCore。EngineCore 的 step() 是驱动循环:Scheduler 决策——这一轮哪些请求跑、每个推进多少 token,产出 SchedulerOutput;Executor 分发到一个或多个 Worker;ModelRunner 执行——准备 InputBatch、调 attention backend 与模型前向、采样,产出 ModelRunnerOutput;EngineCore 把结果交回 Scheduler 更新请求状态,并把 EngineCoreOutputs 送回前端 detokenize、流式返回。传递的是什么:Scheduler 与 ModelRunner 之间传的不是 tensor 而是元数据——每个请求这一轮的 token 数、block table、slot mapping、采样参数;Worker 之间传的是激活(TP 的 all-reduce、PP 的 P2P);前后端之间传的是 token id。详见第二至五章。 ↩

这篇对你有用?

本文由 arganzheng 创作,采用 CC BY 4.0 许可协议。在保留原文作者、署名以及完整原文链接(https://arganzheng.life/vllm-request-lifecycle-overview.html)的前提下,欢迎各种形式的转载、翻译或商业引用。


COMMENTS

评论存放在 GitHub Discussions, 用 GitHub 账号登录即可发表,支持 Markdown。 想针对正文某句话说?选中那段文字,点浮出的「评论」即可划线评论;觉得哪里写错了,发表时勾上「同时提交 Issue」。 有人回复你时 GitHub 会按你的通知设置发邮件,不用守在这里。

×