课程 08 · 生产实现
vLLM v1 的 vllm/v1/ 有 491 个 Python 文件、208,340 行。但如果你已经读懂 nano-vllm,你会发现骨架几乎一样——多出来的是工程约束。
| nano-vllm | vLLM v1 | |
|---|---|---|
| Python 文件 | 21 | 491(整个仓库 3226) |
| 代码行数 | 1,450 | 208,340 |
| 调度器 | 92 行 | 3,869 行 |
| 「引擎」单文件 | 90 行 | 3,094 行(engine/core.py) |
| 模型支持 | 1 个(Qwen3) | 数百个架构 + 多模态 |
| 硬件 | NVIDIA 单机 | NVIDIA / AMD / TPU / CPU / Ascend… |
多出来的部分,绝大部分不是在「理解原理」上变复杂,而是在处理现实世界的约束:多模态输入、投机解码、前缀缓存的淘汰、KV 传输、租户隔离、可观测性、autotune、睡眠唤醒……
读 vLLM 不要试图「读完」。按 nano-vllm 的五个部件去找对应的实现,看到同名概念就停——你已经知道它是干什么的了,只需要知道它在工程上被拆成了几层。
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-vllm | vLLM v1 | 差别 |
|---|---|---|
scheduler.schedule() | scheduler.schedule() | 返回 SchedulerOutput 而不是序列列表 |
model_runner.call("run") | executor.execute_model() + sample_tokens() | 前向与采样拆成两步,可异步 |
scheduler.postprocess() | scheduler.update_from_output() | 逻辑等价,名字更准确 |
Executor + Workervllm/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 就是它的极简版。
这是 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_tokensandnum_tokens_with_spec. … At each step, the scheduler tries to assign tokens to the requests so that each request'snum_computed_tokenscan catch up itsnum_tokens_with_spec.”
翻译过来:每条请求自己有一个「已算 token 数」,调度器每轮只是给它分配一些 token 预算,让它往前追。 于是:
num_tokens_with_spec 比实际 token 多几个 draft。num_computed_tokens 一开始就不是 0。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 空转就是浪费。
| nano-vllm | vLLM v1 | 职责 |
|---|---|---|
LLMEngine.generate | LLMEngine.step + AsyncLLM | 对外接口、流式输出 |
step() | EngineCore.step() | 一轮调度 → 执行 → 回写 |
Scheduler(92 行) | Scheduler(3869 行) | 决定这轮算什么 |
BlockManager | KVCacheManager + BlockPool + KVCacheCoordinator | 块分配、前缀缓存、多 KV group |
ModelRunner | Worker + GPUModelRunner | 组 batch、跑前向 |
Sequence | Request | 请求状态 |
| 共享内存 + Event | EngineCoreClient(ZMQ / IPC)+ MPClient | 前端 ↔ 引擎核通信 |
tensor_parallel_size | 并行配置(TP/PP/DP/EP) | 分布式策略 |
EngineCoreProc单进程版本(EngineCore)已经完整,但生产部署时前端和引擎核通常分进程:
EngineCoreProc[core.py L1299] 因此有一个 run_busy_loop():不断从输入队列取请求、执行 step、把输出推回队列。这和 nano-vllm 里 worker 的 loop() 思路一致,只是通道从共享内存换成了 ZMQ。
打开本地 vendor/vllm/,找到下面三样东西在 vLLM 里的位置(提示:都在 vllm/v1/ 下):
block_manager.can_allocate 对应哪个方法?scheduler.postprocess 对应哪个方法?model_runner.call(跨进程调方法)对应哪一层?① KVCacheManager.get_computed_blocks()(找前缀命中)+ allocate_slots()(真正分配)。
② Scheduler.update_from_output()。
③ Executor.execute_model() → Worker;跨进程的「调方法」被换成了「执行一个 SchedulerOutput」。
「vLLM v1 和上一代(或教学实现)最大的区别是什么?」
「v1用每请求进度与token预算统一表达prefill、decode和draft验证,但仍有模型与阶段相关分支。num_computed_tokens在异步时可能含乐观在途工作,返回后还需修正。前缀查询、KV分配、forward与采样分别有自己的契约。」详见27课。
建议你先做一件事:在 vendor/vllm/vllm/v1/core/sched/scheduler.py 里搜 def schedule,只读函数开头的注释和前 120 行。读不懂没关系,下一课我们逐段讲。