vLLM + verl 源码学习地图:推理引擎与 RL 后训练框架各怎么读
如果只挑一份源码代表当前大模型的工程前沿,我选 vLLM;如果要代表能力前沿(模型为什么越来越聪明),答案是 verl。这两个仓库刚好是一枚硬币的两面:vLLM 回答”模型怎么跑得快”,verl 回答”模型怎么练得强”——而且 verl 的 rollout 引擎就是 vLLM,读完前者再读后者,很多设计会自动对上号。
这篇是两仓的源码学习地图,不做逐行精读。版本钉死在:
- vLLM v0.25.1:约 73 万行 Python + 约 8.9 万行 CUDA/C++(csrc/)
- verl v0.8.0:约 9.3 万行 Python
所有文件链接都固定在这两个 tag 上,随时可以点开复核。
0. 读大仓库的三条心法
第一条:带着一条主链路读,不要从第一行读起。73 万行代码按目录顺序读等于自杀。正确姿势是先钉住一条”请求从进来到出去”的执行链,其余模块都挂在这条链上按需展开。
第二条:先跑起来,再读代码。跑通一个最小例子,把日志开到 debug,看打印顺序,比干读快得多。
第三条:区分”骨架代码”和”叶子代码”。调度器、Worker 抽象、注册表是骨架,值得逐行读;几百个模型实现、几十个 kernel 变体是叶子,读一个代表就够。
1. vLLM:推理引擎怎么读
1.1 目录地图
先把 vllm/ 下的一级目录分个类:
| 目录 | 职责 | 学习优先级 |
|---|---|---|
v1/ | V1 引擎:调度、KV 管理、执行、注意力后端 | ★★★ 必读 |
model_executor/ | 模型注册表、模型实现、通用算子层 | ★★★ 必读 |
distributed/ | TP/PP/EP 并行、KV 传输(PD 分离) | ★★☆ |
csrc/(仓库根) | 自定义 CUDA kernel | ★★☆ 选读 |
entrypoints/ | LLM 类与 OpenAI API server 入口 | ★☆☆ 过一遍 |
engine/ | V0 时代残留,正被 V1 取代 | 跳过 |
lora/、multimodal/、compilation/ | 专题模块 | 按需 |
1.2 五大子系统与必读文件
调度与 KV cache 管理(vLLM 的灵魂):
vllm/v1/core/sched/scheduler.py—Scheduler.schedule(),continuous batching 的真身:每一步按 token budget 决定谁跑、跑多少vllm/v1/core/kv_cache_manager.py—KVCacheManager,PagedAttention 的”页表”vllm/v1/core/block_pool.py— 块复用与哈希匹配,prefix caching 在这里发生
执行层:
vllm/v1/executor/abstract.py— Executor 抽象,单机/Ray 多机的分叉点vllm/v1/worker/gpu_model_runner.py—GPUModelRunner,forward、采样、CUDA graph 都在这里,全仓最长的文件之一
注意力后端:
vllm/v1/attention/selector.py—get_attn_backend(),理解 FlashAttention/FlashInfer/MLA 怎么被选中vllm/v1/attention/backends/registry.py— 后端注册表
模型层:
vllm/model_executor/models/registry.py— 模型注册表,“支持一个新模型”从这里开始vllm/model_executor/models/llama.py— dense 模型范本vllm/model_executor/models/deepseek_v2.py— MoE + MLA 范本,前沿架构在推理侧长什么样
自定义算子(csrc/):csrc/attention/、csrc/moe/、csrc/quantization/ 三个目录,建议读懂一个 kernel 的内存访问模式即可,不必全读。
1.3 高级特性索引
| 特性 | 位置 |
|---|---|
| Speculative decoding(EAGLE/Medusa) | vllm/v1/spec_decode/ |
| PD 分离(disaggregated prefill) | vllm/distributed/kv_transfer/ |
| 并行状态(TP/PP/EP) | vllm/distributed/parallel_state.py |
| 量化(FP8/AWQ/GPTQ) | vllm/model_executor/layers/quantization/ |
| 结构化输出 | vllm/v1/structured_output/ |
| LoRA | vllm/lora/ |
1.4 三阶段路线
flowchart LR
A[阶段一: 数据流<br/>engine/core.py → scheduler.py<br/>→ gpu_model_runner.py] --> B[阶段二: 内存与计算<br/>kv_cache_manager.py → block_pool.py<br/>→ llama.py → attention selector]
B --> C[阶段三: 高级专题<br/>spec_decode / parallel_state<br/>/ kv_transfer / quantization]
- 阶段一(数据流):
v1/engine/core.py的 step 循环 →scheduler.py→gpu_model_runner.py。目标是能口述”一个请求的一生”。我之前写过一篇逐行版:vLLM 的请求是怎么跑完的,可以当阶段一的精读材料。 - 阶段二(内存与计算):KV 管理两件套 + 一个模型实现 + 注意力后端选择。读完你会明白为什么 PagedAttention 是”操作系统思想搬进推理引擎”这句话不是比喻。
- 阶段三(高级专题):按兴趣挑,spec decode 和 PD 分离是当前最活跃的两块。
最小实验:跑 examples/basic/offline_inference/basic.py(把模型换成小的),开 VLLM_LOGGING_LEVEL=DEBUG,对着日志复核阶段一的链路。
2. verl:RL 后训练框架怎么读
verl 是 HybridFlow 论文的开源实现,核心解决一个矛盾:RL 训练的控制流(PPO/GRPO 的迭代逻辑)想写得像单机脚本一样直白,但计算流(rollout 推理、actor 更新)必须分布在几百张卡上。它的答案是单控制器(single-controller)模式:一个中心进程写控制流,计算通过 Ray 分发给 WorkerGroup。
2.1 目录地图
| 目录 | 行数 | 职责 | 优先级 |
|---|---|---|---|
verl/trainer/ | ~12k | PPO/GRPO 主循环、Hydra 配置 | ★★★ |
verl/workers/ | ~20k | Actor/Critic/Rollout worker、训练引擎 | ★★★ |
verl/single_controller/ | ~2.2k | Ray 封装:WorkerGroup、dispatch | ★★★ 短而关键 |
verl/experimental/ | ~9k | agent loop、多轮 RL、tool calling | ★★☆ |
verl/models/ | ~9.5k | 模型加载适配 | ★☆☆ |
verl/utils/ | ~32k | 工具库 | 按需 |
2.2 一次 GRPO 迭代的控制流
全仓最值得精读的一个方法是 RayPPOTrainer.fit()(约 1362 行处)。一个 for 循环看透 RL 后训练的全部阶段。
flowchart TD
A[fit 主循环: 取一个 batch] --> B[generate_sequences<br/>rollout worker 用 vLLM 采样 n 条回答]
B --> C[compute reward<br/>reward manager 规则打分或模型打分]
C --> D[compute_advantage<br/>core_algos.py 按注册的估计器算优势]
D --> E[update_critic 可选<br/>GRPO 没有 critic]
E --> F[update_actor<br/>FSDP/Megatron worker 反向更新]
F --> G[权重同步回 rollout 引擎]
G --> A
2.3 必读文件
入口与控制流:
verl/trainer/main_ppo_sync.py— 当前入口(main_ppo.py已标记废弃)verl/trainer/ppo/ray_trainer.py—RayPPOTrainer.fit()主循环verl/trainer/config/ppo_trainer.yaml— Hydra 配置树的根
单控制器机制(建议最先读,只有两千行):
verl/single_controller/base/decorator.py—@register装饰器与 dispatch 模式(ONE_TO_ALL、DP 切分等)verl/single_controller/ray/base.py—RayWorkerGroup,控制流到计算流的桥
Worker 与训推混合:
verl/workers/engine_workers.py—ActorRolloutRefWorker(约 434 行处):一个 worker 同时背着训练引擎和推理引擎verl/workers/rollout/vllm_rollout/bucketed_weight_transfer.py— 训练权重怎么灌回 vLLM:ZMQ + IPC 分桶传输,训推混合最硬核的一段
算法层:
verl/trainer/ppo/core_algos.py— 优势估计器注册表:GAE、GRPO(约 267 行处)、Dr.GRPO、GDPO、RLOO、REINFORCE++、REMAX、OPO 全在这一个文件里。读懂这个文件,等于横向读完了 2024 以来主流 RL 算法的演化史。verl/workers/reward_manager/— naive(规则奖励)、dapo、prime 等打分器verl/experimental/agent_loop/— 多轮交互与 tool calling,agentic RL 的前沿位置
2.4 三阶段路线
- 阶段一:
examples/grpo_trainer/README.md→main_ppo_sync.py前 100 行 →fit()通读。目标是能口述”一次 GRPO 迭代发生了什么”。 - 阶段二:
single_controller/全读 →engine_workers.py的ActorRolloutRefWorker→core_algos.py里 GAE 与 GRPO 对比。 - 阶段三:
bucketed_weight_transfer.py权重同步 →experimental/agent_loop/多轮 RL。
最小实验:examples/data_preprocess/gsm8k.py 预处理数据,再挑 examples/grpo_trainer/ 下最小的脚本,把 batch size 和 rollout n 调小跑一步,对着日志复核 fit() 的阶段顺序。
3. 两仓的交汇点:读完 vLLM 再读 verl 的复利
verl 的 rollout 就是嵌在训练循环里的 vLLM 实例,两仓知识在三个位置直接复用:
- 采样效率:GRPO 每个 prompt 要采 n 条回答,rollout 吞吐直接决定训练速度——vLLM 的 continuous batching 和 prefix caching(n 条回答共享同一个 prompt 前缀)在这里兑现成训练加速。
- 权重同步:训练引擎(FSDP/Megatron 的分片布局)和推理引擎(vLLM 的 TP 布局)参数排布不同,
bucketed_weight_transfer.py做的就是这两种布局之间的 resharding。读懂它需要两边的知识各一半。 - 显存博弈:训练态和推理态在同一批 GPU 上分时复用,KV cache 何时释放、训练显存何时腾挪,正是 vLLM 阶段二学的 KV 管理知识换了个战场。
4. 一张总结表
| vLLM v0.25.1 | verl v0.8.0 | |
|---|---|---|
| 回答的问题 | 模型怎么跑得快 | 模型怎么练得强 |
| 一句话架构 | Scheduler 驱动的分页 KV 推理引擎 | single-controller 驱动的训推混合 RL 框架 |
| 最该精读的一个文件 | v1/core/sched/scheduler.py | trainer/ppo/ray_trainer.py 的 fit() |
| 最能体现前沿的位置 | spec_decode/、kv_transfer/、MLA 后端 | core_algos.py 算法注册表、agent_loop/ |
| 建议投入 | 3 个阶段约 2-3 周业余时间 | 骨架小得多,1-2 周可过完主链 |
先读 vLLM 打底,再用 verl 把推理知识”接进”训练闭环——这是我能给出的最短复利路径。