课程 08 · 生产实现

从 1450 行到 20 万行

vLLM v1 的 vllm/v1/ 有 491 个 Python 文件、208,340 行。但如果你已经读懂 nano-vllm,你会发现骨架几乎一样——多出来的是工程约束。

约 55 分钟 源码:vllm/v1/engine/ · vllm/v1/core/ 前置:01–07

2026-11-08 源码校正

本页已按本地固定commit修正相关描述。三框架对照 · 带行号的离线证据 · 核查与勘误详情。

1先看规模差在哪

nano-vllmvLLM v1
Python 文件21491(整个仓库 3226)
代码行数1,450208,340
调度器92 行3,869 行
「引擎」单文件90 行3,094 行(engine/core.py)
模型支持1 个(Qwen3)数百个架构 + 多模态
硬件NVIDIA 单机NVIDIA / AMD / TPU / CPU / Ascend…

多出来的部分,绝大部分不是在「理解原理」上变复杂,而是在处理现实世界的约束:多模态输入、投机解码、前缀缓存的淘汰、KV 传输、租户隔离、可观测性、autotune、睡眠唤醒……

心态

读 vLLM 不要试图「读完」。按 nano-vllm 的五个部件去找对应的实现,看到同名概念就停——你已经知道它是干什么的了,只需要知道它在工程上被拆成了几层。

2三层结构

前端 LLMEnginetokenize · 输出处理 · 统计
→
EngineCore调度 · 前向 · 采样
→
Executor + Worker真正碰 GPU 的进程

nano-vllm 把这三层压在一个 LLMEngine 类里,vLLM 把它们拆开,因为中间那层要能跨进程、跨机器部署,还要能异步跑。

前端:LLMEngine.step()

vllm/v1/engine/llm_engine.py · L319–372def step(self):
    outputs = self.engine_core.get_output()                  # 1) 从引擎核取结果
    processed_outputs = self.output_processor.process_outputs(outputs.outputs, ...)   # 2) 解码、判停、组响应
    self.engine_core.abort_requests(processed_outputs.reqs_to_abort)                   # 3) 停字符串等触发的取消
    self.logger_manager.record(...)                           # 4) 指标
    return processed_outputs.request_outputs

前端只做「IO 与语义」:把 token id 变成文字、判断 stop string、组装 OpenAI 风格的响应。它不碰 GPU,也不决定调度。

引擎核:EngineCore.step()

vllm/v1/engine/core.py · L710–740def step(self):
    scheduler_output = self.scheduler.schedule(self._should_throttle_prefills())
    future = self.model_executor.execute_model(scheduler_output, non_block=True)   # 异步提交
    grammar_output = self.scheduler.get_grammar_bitmask(scheduler_output)
    model_output = self._wait_for_output(future)
    if model_output is None:
        model_output = self.model_executor.sample_tokens(grammar_output)            # 采样单独一步
    self._process_aborts_queue()
    engine_core_outputs = self.scheduler.update_from_output(scheduler_output, model_output)
    return engine_core_outputs, scheduler_output.total_num_scheduled_tokens > 0

对照第 01 课的三行 step(),一一对应:

nano-vllmvLLM v1差别
scheduler.schedule()scheduler.schedule()返回 SchedulerOutput 而不是序列列表
model_runner.call("run")executor.execute_model() + sample_tokens()前向与采样拆成两步,可异步
scheduler.postprocess()scheduler.update_from_output()逻辑等价,名字更准确

执行器:Executor + Worker

vllm/v1/executor/
├── uniproc_executor.py     # 单进程单卡
├── multiproc_executor.py   # 多进程(TP/PP),本机
└── ray_executor_v2.py      # 跨机、弹性调度

Worker 是每张卡上的执行进程(vllm/v1/worker/ 有 26 个文件),它持有模型、KV cache 和 CUDA Graph,负责把 SchedulerOutput 变成一次真正的前向。nano-vllm 的 ModelRunner 就是它的极简版。

3三个新概念

① 用 token 预算代替「阶段」

这是 vLLM 与 nano-vllm 最根本的差别。vLLM 调度器开头有一段注释[scheduler.py L708–717],值得逐字读:

“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.”

翻译过来:每条请求自己有一个「已算 token 数」,调度器每轮只是给它分配一些 token 预算,让它往前追。 于是:

nano-vllm 的 is_prefill 布尔值,在 vLLM 里被每请求的计数器取代了。这就是「简化版教你概念、生产版给你一般化」的典型例子。

② SchedulerOutput:调度与执行的契约

vllm/v1/core/sched/output.py · L245–class SchedulerOutput:
    scheduled_new_reqs: list[NewRequestData]        # 第一次见到的请求,传完整数据
    scheduled_cached_reqs: CachedRequestData       # 见过的,只传 diff
    num_scheduled_tokens: dict[str, int]           # 每条请求这轮算多少
    total_num_scheduled_tokens: int
    scheduled_spec_decode_tokens: dict[str, list[int]]
    finished_req_ids: set[str]                     # 通知 worker 释放状态

注意 scheduled_new_reqs / scheduled_cached_reqs 的区分:worker 进程里缓存了请求数据,只有新请求才需要传完整字段。这是跨进程通信的成本意识——nano-vllm 用 Sequence.__getstate__ 做的是同一件事,只是更粗暴。

③ 异步:调度不等前向

future = self.model_executor.execute_model(scheduler_output, non_block=True)
...
model_output = self._wait_for_output(future)

提交前向后,主进程立刻继续做别的事(算 grammar bitmask、处理 abort 队列),等结果时再阻塞。这就是 step_with_batch_queue 和 async_scheduling 的基础:CPU 侧的调度与 GPU 侧的计算重叠。

nano-vllm 是严格串行的:run() 返回之前什么都做不了。对 1.5 秒一次 decode 的小模型无所谓,对 7B 以上、每次 decode 几十毫秒的服务,CPU 空转就是浪费。

4一张映射表

nano-vllmvLLM v1职责
LLMEngine.generateLLMEngine.step + AsyncLLM对外接口、流式输出
step()EngineCore.step()一轮调度 → 执行 → 回写
Scheduler(92 行)Scheduler(3869 行)决定这轮算什么
BlockManagerKVCacheManager + BlockPool + KVCacheCoordinator块分配、前缀缓存、多 KV group
ModelRunnerWorker + GPUModelRunner组 batch、跑前向
SequenceRequest请求状态
共享内存 + EventEngineCoreClient(ZMQ / IPC)+ MPClient前端 ↔ 引擎核通信
tensor_parallel_size并行配置(TP/PP/DP/EP)分布式策略

5为什么要有 EngineCoreProc

单进程版本(EngineCore)已经完整,但生产部署时前端和引擎核通常分进程:

EngineCoreProc[core.py L1299] 因此有一个 run_busy_loop():不断从输入队列取请求、执行 step、把输出推回队列。这和 nano-vllm 里 worker 的 loop() 思路一致,只是通道从共享内存换成了 ZMQ。

6练习 A · 找对应关系

打开本地 vendor/vllm/,找到下面三样东西在 vLLM 里的位置(提示:都在 vllm/v1/ 下):

  1. nano-vllm 的 block_manager.can_allocate 对应哪个方法?
  2. nano-vllm 的 scheduler.postprocess 对应哪个方法?
  3. nano-vllm 的 model_runner.call(跨进程调方法)对应哪一层?
展开:参考答案

① KVCacheManager.get_computed_blocks()(找前缀命中)+ allocate_slots()(真正分配)。

② Scheduler.update_from_output()。

③ Executor.execute_model() → Worker;跨进程的「调方法」被换成了「执行一个 SchedulerOutput」。

7练习 B · 闭卷测验

8面试复述卡

「vLLM v1 和上一代(或教学实现)最大的区别是什么?」

展开:30 秒版本

「v1用每请求进度与token预算统一表达prefill、decode和draft验证,但仍有模型与阶段相关分支。num_computed_tokens在异步时可能含乐观在途工作,返回后还需修正。前缀查询、KV分配、forward与采样分别有自己的契约。」详见27课。

建议你先做一件事:在 vendor/vllm/vllm/v1/core/sched/scheduler.py 里搜 def schedule,只读函数开头的注释和前 120 行。读不懂没关系,下一课我们逐段讲。