vLLM V1 源码阅读笔记

本文基于 vLLM V1 (v0.18.x) 源码阅读整理,记录了从请求进入到 token 输出的完整链路。 完整的架构图可下载 vllm-architecture.drawio 在 draw.io 中查看。 调度过程详解图可下载 vllm-scheduler-64videos.drawio 查看 64 路视频的调度全景。

整体架构概览

vLLM V1 采用多进程架构,核心由三类进程组成:

  1. 主进程 (Main Process) — API Server + 请求预处理 + 输出后处理
  2. EngineCore 子进程 — Scheduler 调度 + KV Cache 管理 + 执行器编排
  3. Worker 进程 — 每个 GPU 一个进程,负责模型推理(Tensor Parallel)

进程间通过 ZMQ (input/output socket) 进行异步通信,EngineCore 与 Worker 之间则使用基于 multiprocessing.shared_memoryshm_broadcast 环形缓冲 + RPC 机制。

一、主进程 (Main Process)

1.1 启动入口

主进程运行 vllm.entrypoints.openai.api_server,由 Uvicorn + FastAPI 驱动。启动流程:

api_server
  └─ lifespan: build_async_engine_client (协程, contextmanager)
       └─ build_async_engine_client_from_engine_args
            └─ AsyncLLM.from_vllm_config(vllm_config)
                 └─ AsyncLLM.__init__()

1.2 AsyncLLM 初始化

AsyncLLM.__init__ 创建以下核心组件:

组件 职责
Renderer (BaseRenderer) 聊天模板渲染、异步 tokenize(AsyncMicrobatchTokenizer)、异步多模态数据加载 → 生成 ProcessorInputs
InputProcessor ProcessorInputs 封装为 EngineCoreRequest(校验参数、处理多模态 hash/placeholder 等)
OutputProcessor detokenize EngineCoreOutputsRequestOutput,检查 stop conditions
IOProcessor (可选插件) 通过 io_processor_plugin 配置加载,用于 Pooling 等特殊场景的自定义 I/O 预处理/后处理,默认 None
EngineCoreClient 与 EngineCore 子进程的 ZMQ 通信客户端
# async_llm.py __init__ 核心初始化
self.renderer = renderer = renderer_from_config(self.vllm_config)
self.io_processor = get_io_processor(...)          # 可选插件,多数模型为 None
self.input_processor = InputProcessor(self.vllm_config, renderer)
self.output_processor = OutputProcessor(renderer.tokenizer, ...)
self.engine_core = EngineCoreClient.make_async_mp_client(...)

Renderer 是 V1 新引入的关键抽象层,位于 vllm/renderers/。不同 tokenizer 类型对应不同实现:

  • HfRenderer:HuggingFace tokenizer(大多数模型)
  • MistralRenderer:Mistral tokenizer
  • QwenVLRenderer 等:特定模型变体

Renderer 内部持有 AsyncMicrobatchTokenizer,将阻塞的 tokenizer 调用卸载到 ThreadPoolExecutor,并支持微批量(micro-batch)合并以提升吞吐。

EngineCoreClient 根据 data_parallel_size 决定使用:

  • 单 DP:AsyncMPClient
  • 多 DP:DPAsyncMPClient / DPLBAsyncMPClient

1.3 请求处理流程(以 OpenAI Chat 请求为例)

一个 Chat Completion 请求的完整链路如下:

HTTP Request
  → OpenAIServingChat.create_chat_completion()
    → render_chat_request()
      → renderer.render_chat_async()          ← ① 异步渲染
        → parse_chat_messages_async()         ← 异步解析消息 + 多模态加载
          → AsyncMultiModalItemTracker        ← fetch_video_async() / fetch_image_async()
            → MediaConnector.load_from_url_async()
              → run_in_executor(global_thread_pool, load_bytes)  ← 线程池解码
        → tokenize_prompts_async()            ← ② 异步 tokenize
          → AsyncMicrobatchTokenizer.encode()
            → run_in_executor(ThreadPoolExecutor, tokenizer)     ← 线程池 tokenize
      → 返回 ProcessorInputs
    → engine_client.generate(engine_prompt, sampling_params)
      → AsyncLLM.add_request()
        → InputProcessor.process_inputs()     ← ③ 封装 EngineCoreRequest
        → engine_core.add_request_async()     ← ④ ZMQ 发送到 EngineCore
      → output_handler 推送结果到 queue       ← ⑤ 异步输出
      → async for chunk in generate(): yield  ← ⑥ SSE 流式返回

① Renderer.render_chat_async() — 异步渲染

  • parse_chat_messages_async() 解析 OpenAI messages 格式,提取文本和多模态 URL
  • 多模态数据(图像/视频/音频)通过 fetch_*_async()run_in_executor(global_thread_pool, ...)媒体线程池中异步加载和解码
  • 多个媒体文件通过 asyncio.gather() 并发加载
  • 渲染聊天模板(apply_chat_template

② tokenize_prompts_async() — 异步 Tokenize

  • 使用 AsyncMicrobatchTokenizer,将阻塞的 tokenizer 调用通过 run_in_executor(ThreadPoolExecutor(max_workers=1), ...) 卸载到单独的 worker 线程
  • 支持微批量(micro-batch):在 batch_wait_timeout_s(默认 2ms)内收集多个请求合并 tokenize,减少调用开销

③ InputProcessor.process_inputs() — 封装请求

  • 接收已处理好的 ProcessorInputs(已包含 prompt_token_idsmm_kwargsmm_placeholders 等)
  • 校验参数、计算 max_tokens、构造 EngineCoreRequest

④ engine_core.add_request_async() — 发送到 EngineCore

  • 通过 ZMQ input_socket 将 EngineCoreRequest (msgpack 序列化) 发送到 EngineCore 子进程

⑤ output_handler (后台 asyncio Task)

  • 持续从 ZMQ output socket 拉取 EngineCoreOutputs
  • 调用 OutputProcessor.process_outputs() 进行 detokenize
  • 将输出按 chunk 分批处理(VLLM_V1_OUTPUT_PROC_CHUNK_SIZE),每批之间 await asyncio.sleep(0) 让出事件循环,避免长时间阻塞
  • 生成 RequestOutput 推送到对应请求的 RequestOutputCollector 队列

关于 GIL:主进程中 tokenizer 和视频解码等 CPU 密集操作不会阻塞 event loop 接收新请求,这依靠两重机制协同工作:

  1. run_in_executor + 线程池:所有阻塞操作都被卸载到工作线程,event loop 线程通过 await 等待结果,期间可以处理其他协程
  2. C/Rust 扩展自动释放 GIL:HuggingFace tokenizers(Rust 实现)、OpenCV(C++ 实现)、NumPy(C 实现)在执行计算时会主动释放 GIL,工作线程不会阻塞 event loop 线程获取 GIL

即使存在少量纯 Python 代码持有 GIL,CPython 3.2+ 的 GIL 调度机制也会每 5mssys.getswitchinterval())强制切换,确保 event loop 线程有机会运行。

二、EngineCore 子进程

每个 DP rank 对应一个 EngineCoreProc 进程,这是 vLLM 的调度核心。

2.1 初始化

EngineCoreProc.__init__()
  ├─ 连接 ZMQ socket (input/output address),与主进程握手建立通信
  └─ EngineCore.__init__() — 创建核心组件
       ├─ MultiprocExecutor (model_executor)
       ├─ Scheduler
       └─ StructuredOutputManager

2.2 IO 线程与主循环

EngineCore 进程内部有 3 个线程协同工作,实现 ZMQ 通信与 GPU 计算的重叠:

┌─ EngineCoreProc 进程 ───────────────────────────────────────────────┐
│                                                                     │
│  Input IO Thread ──► input_queue ──► Main Thread ──► output_queue ──► Output IO Thread
│  (ZMQ recv,                         (busy loop)                      (ZMQ send,
│   msgpack 反序列化,                                                    msgpack 序列化,
│   释放 GIL)                                                           释放 GIL)
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

源码注释写道:“Background Threads and Queues for IO. These enable us to overlap ZMQ socket IO with GPU since they release the GIL, and to overlap some serialization/deserialization with the model forward pass.”

Input IO Thread (process_input_sockets):

  • 从 ZMQ input socket 接收消息并 msgpack 反序列化
  • (request_type, data) 放入 input_queue(Python queue.Queue

Output IO Thread (process_output_sockets):

  • output_queue 取出 EngineCoreOutputs
  • msgpack 序列化后通过 ZMQ output socket 发回主进程

Main Thread 运行 run_busy_loop()

def run_busy_loop(self):
    while self._handle_shutdown():
        self._process_input_queue()    # ① 取请求
        self._process_engine_step()    # ② 调度 + 执行 + 更新 + 输出

_process_input_queue() — 从 input_queue 取出新请求

  • 无工作时阻塞等待(queue.get(block=True)
  • 有工作时非阻塞排空队列(queue.get(block=False)
  • _handle_client_request() 根据请求类型分派:ADDpreprocess_request() → 加入 Scheduler.waiting

_process_engine_step() — 单次引擎步骤

  • scheduler.schedule()SchedulerOutput(本次 step 跑哪些请求、token budget、block 分配)
  • model_executor.execute_model(scheduler_output)ModelRunnerOutput(含 sampled tokens)
  • scheduler.update_from_output() — 更新请求状态,释放已完成请求的 KV blocks
  • 构造 EngineCoreOutputs 放入 output_queue → Output IO Thread 异步发送

2.3 Scheduler 与 KV Cache 管理

schedule() 每个 step 调用:

  1. 优先调度 running 队列(decode 请求)— 计算 token budget,allocate_slots
  2. 调度 waiting 队列(prefill 请求)— get_computed_blocks(prefix cache lookup)→ allocate_slots → 移入 running

KVCacheManager(PagedAttention 核心):

  • free_block_queue(双向链表)管理空闲 block
  • req_to_blocks dict 记录每个请求占用的 block
  • allocate_slots / free / cache_blocks 操作
  • Prefix Caching: hash_request_tokens + find_longest_cache_hit

2.3.1 统一的 Token-Gap 调度模型

V1 Scheduler 最优雅的设计之一是没有”prefill 阶段”和”decode 阶段”的区分。源码注释明确写道:

“There’s no ‘decoding phase’ nor ‘prefill phase’ in the scheduler. Each request just has the num_computed_tokens and num_tokens_with_spec.”

调度器用两个数值统一描述所有请求的状态:

属性 含义
num_computed_tokens 模型已经计算到的位置
num_tokens_with_spec 目标位置(= prompt长度 + 已生成输出 + 投机 token)

每一步,调度器让 num_computed_tokens 尽量追赶 num_tokens_with_spec

  • “Prefill” 不过是差距很大(需要处理整个 prompt)
  • “Decode” 不过是差距为 1(每次生成一个 token)
  • “Chunked Prefill” 是差距很大但被 budget 截断

这个统一模型足够通用,可以覆盖 chunked prefill、prefix caching、speculative decoding 等所有场景。

2.3.2 调度循环与 Token Budget

每次 schedule() 的执行流程:

token_budget = max_num_scheduled_tokens (通常 = max_num_batched_tokens)

Step 1: 遍历 running 队列(decode 优先)
  ├── 每个 request: num_new = target - computed (decode 通常 = 1)
  ├── allocate_slots → 分配 KV blocks
  ├── 失败 → 触发 Preemption(抢占低优先级请求)
  └── budget -= num_new

Step 2: 遍历 waiting 队列(仅在无抢占时)
  ├── 前提: budget > 0 且 len(running) < max_num_seqs
  ├── num_new = min(remaining_prompt, budget)
  ├── chunked prefill: 如果 prompt 太长,分多个 step 完成
  ├── 分配 KV blocks → 移入 running
  └── budget -= num_new

断言: sum(num_scheduled_tokens) <= max_num_scheduled_tokens

2.3.3 关键调度参数对比

参数 限制维度 含义
max_model_len 单请求,整个生命周期 一个请求 prompt + output 的最大总长度,达到后强制停止
max_num_batched_tokens 所有请求,单个 step 一次 forward pass 处理的总 token 数上限(= token budget)
max_num_seqs 所有请求,持续 running 队列的最大长度,限制同时活跃的请求数

简单类比:max_model_len一个请求能跑多远max_num_batched_tokens一步能干多少活max_num_seqs同时能跑多少个

2.3.4 抢占机制 (Preemption)

当 KV block 分配失败时,调度器逐步抢占优先级最低的请求来释放空间:

while allocate_slots 失败:
  if PRIORITY 策略:
    preempt max(running, key=(priority, arrival_time))  # 优先级最低的
  else (FCFS):
    preempt running.pop()  # 最晚加入的

被抢占的请求:
  ① 释放所有 KV blocks
  ② num_computed_tokens 重置为 0
  ③ 放回 waiting 队列头部(优先重新调度)

三、Worker 进程

每个 GPU 一个 Worker 进程,通过 Tensor Parallel 协同工作。

3.1 启动与初始化

MultiprocExecutor (EngineCore 进程中)
  └─ spawn WorkerProc × local_world_size (= TP × PP × PCP)
       └─ Worker init (每个 GPU)
            ├─ init_device() — 设置 CUDA device, 分布式通信组 (NCCL)
            ├─ model_runner.load_model() — 实例化模型架构, 加载权重, torch.compile()
            └─ initialize_cache() — profiling forward pass → 分配 KV cache tensors → warmup CUDA graphs

Worker 进程进入 worker_busy_loop,通过 rpc_broadcast_mq.dequeue 等待来自 MultiprocExecutor 的消息(func + 参数),执行后 handle_output

shm_broadcast: 基于 multiprocessing.shared_memory 的多槽环形缓冲 + 内存栅栏/每读端完成位 + SpinCondition 唤醒。pickle 后小对象整块进环,大对象打 overflow 标并用本机 ZMQ IPC 传 multipart;多机时再配合 TCP XPUB/SUB。

3.2 GPUModelRunner.execute_model()

每次 step 的执行流程:

① _update_states()

  • prune 已结束请求,更新 InputBatch metadata
  • 更新 block table(paged KV 索引)

② _prepare_inputs()

  • CPU → GPU: input_ids, positions, slot_mapping
  • 构造 attention metadata(FlashAttention / FlashInfer)

③ model.forward()

  • eager 模式或 CUDA Graph replay
  • 所有序列拼接为超长序列(Continuous Batching)
  • PagedAttention kernel: 读写 KV cache blocks

④ gather last-token hidden states → compute logits

⑤ Sampler: 从 logits 采样 token

  • 支持 greedy / temperature / top-p / top-k / guided decoding

返回 SamplerOutputEngineCoreOutputs

四、多模态输入预处理

V1 将多模态预处理全部异步化,通过线程池避免阻塞 event loop 和 GPU Worker。

4.1 请求解析(异步路径)

HTTP 请求进入 OpenAIServingChat 后,走异步解析链路:

parse_chat_messages_async() — 解析 OpenAI messages 格式

遍历消息内容,遇到多模态 URL 时不立即加载,而是向 AsyncMultiModalItemTracker 注册一个协程(coroutine):

# chat_utils.py — AsyncMultiModalContentParser
async def _video_with_uuid_async(self, video_url, uuid):
    video = await self._connector.fetch_video_async(video_url) if video_url else None
    return video, uuid

def parse_video(self, video_url, uuid=None):
    coro = self._video_with_uuid_async(video_url, uuid)  # 注册协程,不立即执行
    placeholder = self._tracker.add("video", coro)

AsyncMultiModalItemTracker.resolve_items() — 并发加载所有多模态数据

resolved_items_by_modality = {
    modality: await asyncio.gather(*coros)  # 所有 image/video/audio 并发加载
    for modality, coros in self._items_by_modality.items()
}

③ 媒体加载线程池 — 真正的 IO 和解码在线程池中执行

# connector.py
global_thread_pool = ThreadPoolExecutor(
    max_workers=envs.VLLM_MEDIA_LOADING_THREAD_COUNT  # 可配置
)

async def load_from_url_async(self, url, media_io, ...):
    # HTTP 下载使用 aiohttp(纯异步,不占线程)
    data = await connection.async_get_bytes(url)
    # 解码在线程池中执行(OpenCV/NumPy 释放 GIL)
    return await loop.run_in_executor(global_thread_pool, media_io.load_bytes, data)
操作 执行方式 GIL 行为
HTTP 下载视频/图片 aiohttp 异步 I/O 不持有 GIL
视频解码 (cv2.VideoCapture) run_in_executor(global_thread_pool) C++ 扩展释放 GIL
图片解码 (PIL.Image.open) run_in_executor(global_thread_pool) C 扩展释放 GIL
NumPy 数组操作 (resize 等) 同上线程池 C 扩展释放 GIL
Tokenize run_in_executor(ThreadPoolExecutor(1)) Rust FFI 释放 GIL

④ Renderer.render_chat_async() — tokenize + 组装

tokenize 通过 AsyncMicrobatchTokenizer 在独立线程中完成后,与多模态数据一起组装为 ProcessorInputs

4.2 多模态缓存

mm_processor_cache(主进程缓存):

  • key = hash(mm_data) ← 同一图/视频跨请求复用
  • 避免重复 CPU 解码 / resize

4.3 构造 EngineCoreRequest

prompt_token_ids: list[int]          ← 已含 mm placeholder token
mm_features: list[MultiModalFeature] ← 每个 mm 对象的 pixel_values / audio features
mm_hashes: list[str]                 ← 用于 encoder_cache 查找
mm_placeholders: list[PlaceholderRange] ← 在 token 序列中的位置信息

msgpack 序列化 → ZMQ input_socket → EngineCore 进程。

4.4 Encoder Cache (GPU 侧)

在 EngineCore 进程中,preprocess_request() 检查 mm_hash 是否在 encoder_cache 中:

  • 命中 → 直接取已有 GPU embedding,跳过视觉编码
  • 未命中 → 将 pixel_values 等打入 EngineCoreRequest,在 Worker 中由 Vision Encoder 处理

4.5 model.forward() 执行阶段

Vision Encoder:  pixel_values → vision embeddings (e.g. 4096 dims/image)
Projection Layer: vision_embeddings → LLM hidden_size
Merge: text token hidden states + vision hidden states → 统一 hidden sequence

合并后进入标准 Transformer decoder 流程。

五、Continuous Batching + Chunked Prefill

5.1 核心设计

每个 Engine Step 都有一个固定 Token Budget (max_num_batched_tokens, 默认 2048+)。Scheduler 在 budget 内打一个混合 batch:先塞满所有 decode 请求(每个占 1 token),剩余 budget 分给 prefill(可 chunk)。

Step 内容
Step 1 R1 Prefill chunk①(512 tokens) + R2 Decode(1 token) + 空余 budget
Step 2 R1 Prefill chunk②(512 tokens) + R2 Decode(1 token) + R3 Prefill chunk①(300 tokens)
Step 3 R1 Decode(1 token, prefill done✓) + R2 Decode(1 token) + R3 Prefill chunk②(200 tokens)
Step 4 R1 Decode(1 tok) + R4 新请求 Prefill(100 tokens)

5.2 序列 Flatten

Forward Pass 内部,所有序列被 flatten 拼接为一条超长序列(无 padding)。以 Step 2 为例:

input_ids: [R1_tok[512..1023], R2_tok[n], R3_tok[0..299]]

attention mask 保证每个序列只 attend 自身。slot_mapping 记录每个 token 对应的 KV cache 物理 block slot(PagedAttention 寻址)。

5.3 KV Cache 管理

  • Step 1: R1 分配 2 blocks, R2 无新分配
  • Step 2: R1 +2 blocks, R2 无新, R3 +2 blocks
  • Step 3: R1 decode +1tok, R2 +1tok, R3 +2 blocks, R4 +1 block

5.4 抢占 (Preemption)

KV Cache 不足时,V1 默认 RECOMPUTE 模式(V0 支持 SWAP):将低优先级 running 请求踢回 waiting 队列,释放其 KV blocks。下次调度时重新从头 prefill(开销低于 swap to CPU)。

5.5 Continuous Batching 核心特性

  • 序列在 forward pass 中被 flatten 拼接(无 padding)
  • 每个 step 结束后,已完成的请求立即被移出,新请求可在下个 step 加入
  • V1 取消了 prefill/decode 的人为分界,统一用 {req_id: num_tokens} 表示调度决策
  • KV Cache 用 PagedAttention 管理,物理块按需分配/回收

5.6 实例:Qwen3VL 64路视频理解的调度过程

完整的调度过程图可下载 vllm-scheduler-64videos.drawio 在 draw.io 中查看。

以 Qwen3VL 处理 64 个视频理解请求为例,展示 Continuous Batching 在真实多模态场景下的调度行为。

Step-by-Step 调度过程

Step 1 — 首批进入:

Budget: 8192
├── R1: prefill 7000 (一次完成)        → budget 剩余 1192
└── R2: prefill 1192 (分块! 仅部分)    → budget = 0
Batch size: 2, Waiting: 62

Step 2 — 混合调度开始:

Budget: 8192
├── R1: decode 1   (已完成 prefill)    → budget 8191
├── R2: prefill 5808 (完成剩余部分)    → budget 2383
└── R3: prefill 2383 (新请求, 分块)    → budget = 0
Batch size: 3, Waiting: 61

这就是 Continuous Batching 的核心:R1 在 decode,R2 在继续 prefill,R3 同时开始 prefill,三种状态共存于同一个 batch!

Step 4 — 阶梯模式形成:

Budget: 8192
├── R1-R3: decode × 3 = 3              → budget 8189
├── R4: prefill 3427 (完成剩余)        → budget 4762
└── R5: prefill 4762 (新请求, 分块)    → budget = 0
Batch size: 5, Waiting: 59

Step 6 — 加速效应:

Budget: 8192
├── R1-R5: decode × 5 = 5              → budget 8187
├── R6: prefill 1050 (完成)            → budget 7137
├── R7: prefill 7000 (一次完成!)       → budget 137
└── R8: prefill 137 (分块)             → budget = 0
Batch size: 8, Waiting: 56

关键洞察:decode 请求越多 → 每个只消耗 1 token → 剩余 budget 越大 → 新请求 prefill 越快!

Step 40+ — 全速运行(running 达到 max_num_seqs=32 上限):

Budget: 8192
├── R1-R32: decode × 32 = 32 (仅占 0.4%!)  → budget 8160
├── R33: prefill 7000 (从 waiting 进入)     → budget 1160
└── R34: prefill 1160 (分块)                → budget = 0

Step 200+ — 请求轮替:

R1 生成完毕 → 释放 slot 和 KV blocks
→ R33-R64 中的下一个立即从 waiting 进入 running
→ 开始 prefill,循环直到所有 64 个请求完成

三阶段全景

阶段 Step 范围 瓶颈 状态
阶梯式接入 Step 1-40 token budget running 从 2 → 32 逐步增长
全速 decode Step 41-200 max_num_seqs 32 个请求全在 decode,新 prefill 充分利用 budget
请求轮替 Step 200+ 请求完成速度 R1 完成 → R33 进入,循环直到 R64

对比:Static Batching vs Continuous Batching

  Static Batching Continuous Batching
调度方式 等一批全部完成再处理下一批 随到随进,完成即走
batch 大小 固定(受限于显存预分配) 动态增减
prefill + decode 严格分开 同一 batch 混合
64路视频估计步数 2批 × (7000+200) ≈ 14,400 步 阶梯并行 ≈ 440 步
GPU 利用率 短请求等长请求,大量空转 几乎满负载

Continuous Batching 在这个场景下可以带来约 30 倍的吞吐量提升。

六、GPUModelRunner V1 vs Model Runner V2 (MRV2)

MRV2 (VLLM_USE_V2_MODEL_RUNNER=1 开启) 目前仍为 experimental;V1 为当前默认。两者共用同一套 Scheduler / Executor / KV Cache 架构,仅 ModelRunner 层不同。

6.1 整体设计哲学

V1 (GPUModelRunner) MRV2 (ModelRunner V2)
单文件 monolith: gpu_model_runner.py ~6283 行 拆分为 ~40 个子模块,最大文件 <1300 行
所有逻辑(通用 + 模型专属)耦合在一起 三大原则:Be Modular / Be GPU-native / Be Async-first
特性逐步 bolt-on,随时间积累技术债 ModelState 抽象:模型专属逻辑与公共路径隔离
难以扩展新模型,新贡献者学习曲线陡峭 从头重写,不继承 V1 复杂性

6.2 Persistent Batch 设计

V1 的问题

persistent state tensor 直接用作 model input tensor(都在 CPU),block table 布局与请求顺序强耦合。这带来几个严重问题:

  • 添加/删除请求 → 需要整张 tensor 重排(complex reorder)
  • 需要 CachedRequestState 作冗余备份
  • async scheduling 下容易引发 race condition
  • 所有 CPU 操作必须在 async barrier 内,灵活性低

MRV2 的解法

将持久状态与输入解耦为两部分:

  • stable state table (CPU, fixed 1024 rows) — 每个 req 在生命周期内固定一行
  • per-step input tensor (GPU) — Triton gather kernel 按当前顺序组装
# MRV2: 持久 state 与 input 解耦
self.states[req_idx] = new_req.data       # 写持久 state (非 pinned)
tmp_states = self.states.pin_memory()      # 每步拷贝到临时 pinned
states = tmp_states.to('cuda', non_blocking=True)  # H2D
input_tensors = triton_gather(states, schedule_indices)  # GPU gather

解耦好处:

  • 添加/删除请求只改 state table 一行,O(1)
  • 消除 CachedRequestState 冗余备份
  • StagedWriteTensor: CPU 写 state, GPU 读 tmp copy → 无 race
  • input_ids / positions / seq_lens / block_table 全在 GPU 组装
  • 不再需要 async barrier

6.3 Input Preparation

V1 MRV2
CPU (Python/NumPy) → copy to GPU GPU-native Triton gather kernel

6.4 Async Scheduling

V1 MRV2
Retrofit + async barrier Design-first,zero CPU-GPU sync

6.5 Sampler

V1 MRV2
PyTorch softmax + multinomial Triton Gumbel-Max,内存更省

6.6 ModelState 抽象 (MRV2 核心创新)

class ModelState(ABC):
    def add_request(self, req: EngineCoreRequest): ...
    def remove_request(self, req_id: str): ...
    def get_mm_embeddings(self, req_id: str) -> Tensor: ...
    def prepare_inputs(self, scheduler_output) -> dict: ...

模型扩展方式对比:

  • V1: override 方法,无统一抽象
  • MRV2: ModelState ABC,职责清晰

Spec Decoding + Async 支持:

  • V1: 难以同时支持,需要 hack
  • MRV2: 天然支持,GPU prep 直接消费 rejection output

6.7 性能对比 (官方 benchmark)

Qwen3-0.6B · 1×GB200(小模型大 GPU → host overhead 占比大)

指标 V1 MRV2
输出吞吐量 ~16K tok/s ~25K tok/s (+56.2% ↑)

GLM-4.7-FP8 · 4×GB200 · MTP spec decoding

MRV2 实现 -6.3% TPOT(zero-sync spec decoding)。

6.8 当前功能覆盖 (v0.18.0)

V1 (完整生产可用) MRV2 (Experimental)
✅ 所有 decoder-only 模型 ✅ 标准 decoder-only 模型推理
✅ MoE (Mixtral / DeepSeek) ✅ Eagle / Eagle3 / MTP spec decoding
✅ 多模态 VLM (图像/视频/音频) ✅ Async scheduling (zero sync)
✅ Full spec decoding ✅ 基础多模态
✅ LoRA / EPLB / DBO ❌ Linear attention (Qwen3.5)
✅ Structured output ❌ MTP 以外的 spec decoding

6.9 核心差异一览表

维度 V1 (GPUModelRunner) MRV2 (ModelRunner V2)
代码规模 单文件 ~6283 行 ~40 模块,最大 <1300 行
Persistent Batch state 直接 = input,耦合强 stable state table + gather,解耦
Input Preparation CPU (Python/NumPy) → copy to GPU GPU-native Triton gather kernel
Async Scheduling Retrofit + async barrier Design-first,zero CPU-GPU sync
Sampler PyTorch softmax + multinomial Triton Gumbel-Max,内存更省
模型扩展 override 方法,无统一抽象 ModelState ABC,职责清晰
Spec Decoding + Async 难以同时支持,需 hack 天然支持
状态 ✅ 默认,生产就绪 ⚠ Experimental,部分功能缺失

七、torch.compile Backend:默认 Inductor vs VllmBackend

vLLM 对 torch.compile 进行了深度定制,实现了 VllmBackend 以替代默认的 Inductor backend。

7.1 触发入口

默认 Inductor VllmBackend(vLLM 定制)
torch.compile(model) torch.compile(model, backend=VllmBackend(...))
Dynamo 追踪字节码,生成完整 FX 图 Dynamo 追踪后调用 VllmBackend.__call__(graph, example_inputs)
直接将整图交给 Inductor 处理 vLLM 接管整个编译流程
用户无需关心内部切分逻辑 支持 prefix / model_tag 区分多模型部件

7.2 图切分策略 (Graph Splitting)

默认 Inductor VllmBackend
无主动切分,整图交给 Inductor 自动 op fusion 主动 split_graph() 切分
切分完全由 Inductor 自动决定 splitting_ops(如 all_reduce、flash_attention)切割
不感知 all_reduce / attention 等通信算子边界 相邻 splitting op 合并为同一子图,避免碎片化
无法针对 tensor parallel 做精细控制 纯空分配节点(empty)合并到前一分区,防止空 cudagraph

7.3 CUDA Graph 捕获方式

默认 Inductor VllmBackend
整图静态捕获(FULL 模式) 分段 PIECEWISE 捕获
torch.compile(mode="reduce-overhead") 触发整图 cudagraph 每个非 splitting 子图单独包进 CUDAGraphWrapper
要求整图输入形状完全静态 splitting 子图(all_reduce 等)不被捕获,允许主机同步
动态 batch size 场景需要 padding 或 bucketing sym_tensor_indices 追踪动态维度,用 copy_and_call 注入静态 buffer

7.4 编译缓存机制

默认 Inductor VllmBackend
torch._inductor.codecache 内置 compilation_config.cache_dirVLLM_CACHE_ROOT
以 FX 图 hash 作 key 以 FX 图 hash + model_tag 作 key
仅缓存 Inductor 产物(Triton code / .so) 缓存 split 后每段 Inductor 产物 + CUDA Graph metadata

7.5 Post-grad 自定义 Pass

默认 Inductor VllmBackend
只跑 Inductor 内置 pass 注入 fix_functionalization 等自定义 pass
无法干预 attention kernel 选择 可替换 FlashAttention 后端 / 注入 custom op

7.6 动态形状 (Dynamic Shape) 处理

默认 Inductor VllmBackend
SymInt 符号追踪,需要 guard 验证 PiecewiseCompileIntermediate 记录每段输入张量索引
动态 batch → recompile 或 bucketing sym_tensor_indices 标记动态维度 → cudagraph 捕获时只替换动态 tensor

7.7 多模型组件支持

默认 Inductor VllmBackend
单一 compile 调用,不区分模型部件 prefix_str / model_tag 区分 draft model、vision encoder、LLM backbone
所有子图共享同一编译配置 每个部件可独立控制是否 cudagraph、是否 compile

7.8 返回值与序列化

默认 Inductor VllmBackend
返回编译后的 callable(Triton code) 返回 VllmSerializableFunction
torch.save / torch.load 标准序列化 支持 pickle 序列化已编译函数 → 跨进程共享编译产物

八、进程拓扑与数据并行 (DP)

8.1 进程层次模型

vLLM V1 的进程架构是三层嵌套的:

API 进程 (AsyncLLM)
  │
  ├── EngineCore 进程 #0  (DP rank 0)
  │     └── MultiprocExecutor
  │           ├── Worker 进程 #0  (GPU 0) ← TP rank 0
  │           ├── Worker 进程 #1  (GPU 1) ← TP rank 1
  │           ├── Worker 进程 #2  (GPU 2) ← TP rank 2
  │           └── Worker 进程 #3  (GPU 3) ← TP rank 3
  │
  ├── EngineCore 进程 #1  (DP rank 1)
  │     └── MultiprocExecutor
  │           ├── Worker 进程 #4  (GPU 4) ← TP rank 0
  │           └── ...
  │
  └── (可选) DPCoordinator 进程
        └── 负载均衡统计、MoE wave 协调

关键的配置公式:

参数 含义
world_size = TP × PP × PCP每个 DP 副本内的 Worker 数量
data_parallel_size DP 副本数,即 EngineCore 进程数
总 GPU 数 = world_size × data_parallel_size
总 Worker 进程数 = 总 GPU 数(一 GPU 一 Worker)
# config/parallel.py
self.world_size = (
    self.pipeline_parallel_size
    * self.tensor_parallel_size
    * self.prefill_context_parallel_size
)

8.2 典型部署拓扑举例

场景 A:单卡部署(最简)

TP=1, PP=1, DP=1 → 1 个 EngineCore + 1 个 Worker

API 进程 ──ZMQ──► EngineCore ──shm──► Worker (GPU 0)

场景 B:4 卡张量并行

TP=4, PP=1, DP=1 → 1 个 EngineCore + 4 个 Worker

API 进程 ──ZMQ──► EngineCore ──shm──► Worker 0 (GPU 0)
                                  ├──► Worker 1 (GPU 1)
                                  ├──► Worker 2 (GPU 2)
                                  └──► Worker 3 (GPU 3)
                                       ↕ NCCL AllReduce

场景 C:8 卡数据并行

TP=1, PP=1, DP=8 → 8 个 EngineCore,各带 1 个 Worker

API 进程 ──ZMQ──► EngineCore 0 ──► Worker (GPU 0)
           ├───► EngineCore 1 ──► Worker (GPU 1)
           ├───► ...
           └───► EngineCore 7 ──► Worker (GPU 7)
                 ↕
           DPCoordinator (负载均衡)

场景 D:DP + TP 混合(8 卡,DP=2, TP=4)

API 进程 ──ZMQ──► EngineCore 0 (DP rank 0) ──► 4 Workers (GPU 0-3, NCCL)
           └───► EngineCore 1 (DP rank 1) ──► 4 Workers (GPU 4-7, NCCL)

8.3 DP 下 EngineCore 与 Worker 的关系

每个 EngineCore 进程独立管理自己副本内的 Workers,不同 DP rank 之间的 Worker 完全隔离:

# multiproc_executor.py — 每个 EngineCore 内部
for local_rank in range(self.local_world_size):  # local_world_size = TP × PP
    WorkerProc.make_worker_process(
        local_rank=local_rank,
        rank=global_rank,
        ...
    )

跨 DP rank 的协调由 DPCoordinator 进程完成(仅在 data_parallel_size > 1 时启动):

  • 收集各 EngineCore 的队列长度统计,发布给前端做负载均衡
  • MoE 模型的 wave coordination(确保所有 DP rank 同步执行 all-to-all)

8.4 Worker 启动流程

MultiprocExecutor._init_executor()
  ├─ 创建 MessageQueue (shm_broadcast 环形缓冲)
  ├─ for local_rank in range(local_world_size):
  │     WorkerProc.make_worker_process()
  │       └─ multiprocessing.Process(target=WorkerProc.__init__)
  │            ├─ WorkerWrapperBase → 实例化 Worker (gpu_worker.py)
  │            ├─ Worker.init_device()  → CUDA device 设置, NCCL 通信组
  │            └─ Worker.load_model()   → 加载权重, torch.compile
  └─ WorkerProc.wait_for_ready()  → 等待所有 Worker 就绪

Worker 就绪后进入 worker_busy_loop,通过 rpc_broadcast_mq.dequeue 等待 EngineCore 派发 RPC 调用(如 execute_model)。

8.5 通信机制总结

通信对 机制 特点
API 进程 ↔ EngineCore ZMQ (IPC/TCP) + msgpack 跨进程,支持多节点
EngineCore ↔ Worker shm_broadcast (共享内存环形缓冲) 同机低延迟,pickle 序列化
Worker ↔ Worker (TP/PP) NCCL GPU 直连,高带宽
DP rank 协调 DPCoordinator (ZMQ) 统计发布 + wave 同步