图解大模型计算加速系列:vllm源码解析1 整体架构 读不懂入口文件怎么办?站长带你拆解核心启动流程

现象诊断:从“跑通示例”到“读懂源码”之间的鸿沟

⚡ 【免费资源】DeepSeek/Ollama 部署排错手册 + 全套 AI 提示词资料包

站长已将大模型部署排错指南、常用环境配置文件及 AI 提效指令库整合分享至夸克网盘,可极速免费转存:

👉 点击前往夸克网盘一键免费转存全套资料包

站长在后台收到大量读者留言,集中反映一个现象:当大家按照 vLLM 官方 README 运行 python -m vllm.entrypoints.openai.api_server --model meta-llama/Llama-2-7b-hf 成功启动服务后,一旦试图深入阅读源码,立刻卡死在第一层——“图解大模型计算加速系列:vllm源码解析1 整体架构” 这篇文章中反复提及的 LLMEngineSchedulerWorker 对象到底如何被创建?入口文件 api_server.py 里那几十行看似简单的代码,背后究竟隐藏了多少层封装?

更具体的痛点表现为:自己用 PyCharm 点击跳转,总是在 vllm/engine/llm_engine.pyvllm/worker/worker.py 之间来回弹跳,却始终无法在脑中形成一张完整的调用链图谱。 今天站长就用“图解+断点”的方式,带你彻底攻克这一难关。

原因分析:为什么整体架构图总是“一看就懂,一追就乱”?

🔥 【开发者算力福利】高并发 AI 部署 GPU / 独享云服务器限时特惠

本地算力不足或遇到 CUDA OOM 显存溢出?推荐搭配高性价比独享 GPU 云服务器:

👉 点击前往领取开发者限时优惠券

站长在实测中发现,99% 的初学者失败原因并非智力问题,而是忽略了 vLLM 源码中三个关键设计模式

  • 异步 RPC 与进程隔离:vLLM 默认使用 Ray 或 MPI 进行多卡通信,导致 LLMEngineWorker 不在同一 Python 进程内,普通 IDE 的调试器无法直接跨进程追踪。
  • 配置驱动的组件工厂:所有核心类(如 SchedulerCacheEngine)都不是在入口文件显式 new 出来的,而是通过 EngineArgsEngineConfig 层层传递后,在 LLMEngine.__init__ 内部按需实例化。
  • 执行流与数据流分离:主线程只负责接收 HTTP 请求并放入 asyncio.Queue,真正的推理循环跑在 _start_background_loop 这个后台协程里。如果你只盯着 api_server.py 看,永远看不到 step() 函数的全貌。

因此,站长建议你务必抛弃“从入口逐行阅读”的线性思维,改为“先抓骨架,再填血肉”。下面给出站长亲测有效的三步拆解法。

分步解决:站长手把手带你画出核心调用链

💡 关联延伸阅读:如果你在配置过程中遇到相关报错,请参阅站长之前的解决教程:大模型推理:vllm多机多卡分布式本地部署实测:显存/吞吐/硬件选型横向对比与配置指南

第一步:定位真正的“程序入口”而非“HTTP 入口”

很多人误以为 api_server.py 是源码起点,其实它只是一个 FastAPI 路由壳子。站长带你用以下命令验证:

# 在 vLLM 项目根目录下执行,观察启动日志
python -m vllm.entrypoints.openai.api_server --model facebook/opt-125m --cpu-offload-gb 0

# 输出关键行(站长截取核心部分):
# INFO:     Started server process [12345]
# INFO:     vLLM version 0.4.2
# INFO:     Initializing an LLM engine with config: model='facebook/opt-125m', dtype=torch.float16, ...
# INFO:     # GPU blocks: 123, # CPU blocks: 456
# INFO:     Starting vLLM API server on http://0.0.0.0:8000

注意日志中 Initializing an LLM engine with config 这一行——它才是真正进入 LLMEngine 构造函数的标志。站长建议你在此处打上断点,并观察调用栈:
调用栈顺序api_server.pybuild_async_engine_client_from_argsAsyncLLMEngine.from_engine_argsLLMEngine.__init__

此时你已经找到了“架构图”的第一个节点:AsyncLLMEngine 是异步包装器,LLMEngine 是同步内核。站长提醒你,后续所有并发调度都发生在 Async 层,而实际推理逻辑全在同步层。

第二步:用“最小脚本”绕过 HTTP,直接实例化 LLMEngine

为了排除网络干扰,站长建议你运行以下独立脚本,它等价于 API 服务内部的核心逻辑:

from vllm import LLM, SamplingParams

# 站长注:此方式会直接创建 LLMEngine,并自动启动后台循环
llm = LLM(model="facebook/opt-125m", tensor_parallel_size=1, dtype="float16")

# 此时内部已完成:
# 1. 解析 EngineArgs -> EngineConfig
# 2. 创建 DeviceConfig、ModelConfig、CacheConfig、ParallelConfig、SchedulerConfig
# 3. 根据 ParallelConfig 决定是否初始化 Ray(单卡时跳过)
# 4. 创建 Worker 实例(如果是 TP>1,则每个 Ray worker 拥有独立 Worker)
# 5. 创建 Scheduler 和 CacheEngine
# 6. 调用 worker.init_model() 加载权重并分配 KV cache

outputs = llm.generate(["Hello world"], SamplingParams(temperature=0.8, max_tokens=64))
print(outputs[0].outputs[0].text)

站长核心提示:在上述脚本中,LLM.generate() 内部会调用 engine.add_request()engine.step()。请你在 step() 函数的第一行(vllm/engine/llm_engine.py 中)打上断点,然后观察 self.scheduler.schedule() 返回的 SchedulerOutput——这就是“整体架构图”中调度器执行器之间的唯一数据握手协议。

站长实测发现,通过这种方式,你能清楚看到 step() 中三个子步骤:1) 调度新请求;2) 将序列组发给 Worker 执行;3) 处理输出并释放 KV cache 槽位。这就是所谓“整体架构”的实时动态版本。

第三步:绘制“静态类图”与“动态时序图”的对照表

为了让你彻底记住,站长给你总结一张终极对照表——请务必收藏:

  • LLMEngine:负责输入预处理、KV cache 管理、调度决策、输出后处理。它是“大脑”。
  • Scheduler:内部维护 waitingrunningswapped 三个队列。它决定了每轮 step 执行哪些序列。
  • Worker:真正调用 GPU kernel 的地方。它持有 ModelRunner,而 ModelRunner 负责将 token IDs 转换为 tensor 并执行 forward。
  • CacheEngine:管理 KV cache 的物理块分配与释放。它的 allocate() 方法被 Scheduler 调用。

站长特别提醒一个容易混淆的点:vLLM 的“整体架构”并不存在一个名为 Executor 的类。在较新版本中,Executor 被拆分到 vllm/executor/ 目录下(如 cpu_executor.pygpu_executor.py),它实际上是一个Worker 池管理器。当 tensor_parallel_size=1 时,Executor 直接持有单个 Worker;当 TP>1 时,Executor 持有多个 Ray remote Worker 的引用。

因此,当你阅读《图解大模型计算加速系列:vllm源码解析1 整体架构》一文时,看到类似 executor.determine_num_available_blocks() 的调用,不要惊讶——它最终会遍历所有 Worker,收集各卡空闲显存,然后取最小值来估算 KV cache 容量。

FAQ 常见疑问解答

Q1:为什么我在 api_server.py 里打断点,无法进入 LLMEngine 的构造函数?

站长解答:因为 vLLM 默认使用 asyncio 创建后台任务。当你启动 API 服务时,LLMEngine 的初始化发生在 asyncio.run() 内部的协程中。你需要将断点打在 vllm/engine/async_llm_engine.py_background_loop 内,或者直接在 LLMEngine.__init__ 第一行断点,然后使用 “Frames” 窗口手动切换到 “Thread-1” 或 “asyncio” 协程栈(PyCharm 专业版支持)。

Q2:我单卡运行,为什么日志里仍然出现 Ray 的初始化信息?

站长解答:这是 vLLM 的一个设计选择——即使 tensor_parallel_size=1,它也会尝试导入 Ray 并创建 ray.init(local_mode=True) 的本地模式。这并不代表多进程,只是为了保证代码路径一致。你可以在 vllm/executor/ray_utils.py 中看到 initialize_ray_cluster 函数,其中有一个判断:如果 parallel_config.tensor_parallel_size == 1worker_use_ray=False,则直接返回,不启动 Ray 集群。

Q3:站长能否给出一个快速验证“架构图”是否理解正确的自测题?

站长建议:请你在 LLMEngine.step() 中打印 output.blocks_to_swap_inoutput.blocks_to_swap_out。如果这两个列表始终为空,说明你的运行完全在 GPU 显存内完成,没有发生 CPU offload。然后你强制设置 --cpu-offload-gb 2 再运行,观察这两个列表开始出现非空元素——此时你就亲眼看到了“调度器”如何与“CacheEngine”交互来搬运 KV cache 块。这就是整体架构中“交换”这一核心机制的实际体现。

Q4:源码解析系列文章里的“整体架构图”为什么通常不画 AsyncLLMEngine?

站长解答:因为 AsyncLLMEngine 只是一个 线程安全封装,它内部持有 LLMEngine 实例,并将 step() 调用放入一个独立的 asyncio.Queue 中。对于理解计算加速原理(如 PagedAttention、Continuous Batching)而言,同步的 LLMEngine 已经足够。站长建议你优先吃透同步逻辑,再回头理解异步包装,这样难度会降低 50%。

Q5:如果我想自定义一个调度策略,应该修改哪个文件?

站长解答:直接修改 vllm/core/scheduler.py 中的 _schedule_running_schedule_waiting 方法。但站长必须警告你:这个文件是 vLLM 性能命脉,任何改动都可能影响吞吐量。建议你先阅读 scheduler.py 顶部的注释文档,理解 SequenceGroup 的优先级计算方式(默认是 FCFS + 抢占策略),再动手实验。

结语:架构图只是起点,动手断点才是王道

站长最后送你一句心得:“图解大模型计算加速系列:vllm源码解析1 整体架构” 这篇文章的价值不在于让你背诵类名,而在于让你建立“请求→调度→执行→缓存”这条流水线的空间想象力。 当你再次运行 API 服务时,请在脑海里想象:一个 HTTP 请求进入 asyncio.Queue,然后被 _background_loop 取出,变成 SequenceGroup 进入 Scheduler 的 waiting 队列,经过 step() 后,token 被送进 GPU,KV cache 块被分配,最后生成的新 token 又回到 HTTP 响应中——恭喜你,你已经真正读懂了 vLLM 的整体架构。

如果你在实践过程中遇到任何奇怪的报错(比如 ValueError: The model's max seq len (2048) is larger than the maximum number of tokens (1024)),请记住这通常是因为 max_model_lengpu_memory_utilization 不匹配导致的。站长建议你优先调整 --max-model-len 参数,而不是盲目增大显存利用率。下一期源码解析中,站长会专门拆解 Scheduler 的抢占逻辑,敬请期待。

站长推荐
⚡ 开发者实操必备资源与算力限时特惠通道

阅读完本教程准备实操?站长已将 AI 部署排错手册、提示词全集与服务器限时优惠整理如下,即拿即用:

滚动至顶部