课程 10 · 执行层
nano-vllm 的 capture_cudagraph 只能录 decode。vLLM 把「录图」做成了三种模式,还能一边编译一边录——这一课讲清 CUDAGraphMode 到底在权衡什么。
| Executor | 部署形态 | 典型场景 |
|---|---|---|
UniProcExecutor | 单进程单卡 | 本地调试、小模型 |
MultiprocExecutor | 本机多进程 | 单机 TP / PP |
RayExecutorV2 | 跨机、Ray 集群 | 多机、弹性伸缩 |
三者对上层暴露同一个接口:execute_model(scheduler_output) 和 sample_tokens()。调度器完全不知道底下有多少张卡——这是 vLLM 能把「单卡」和「千卡」用同一份调度代码跑起来的原因。
execute_model(scheduler_output)
├─ 把 num_scheduled_tokens 展开成每个 token 的位置
├─ 更新 block table / slot mapping(和 nano-vllm 一模一样)
├─ 选 attention backend,跑前向
└─ 返回 hidden states 或 logits
它和 nano-vllm 的 ModelRunner.run() 是同一件事,只是规模不同:
| nano-vllm | vLLM Worker | |
|---|---|---|
| 模型 | Qwen3 一个文件 | 数百个架构的注册表 + 权重加载器 |
| 注意力 | 写死 FlashAttention | 可插拔 backend(见下节) |
| 图 | 一种录法、只录 decode | 三种模式、可按 decode / mixed 分别配置 |
| 并行 | TP | TP / PP / DP / EP |
| KV | 一个大张量 | 多 group、可跨节点传输 |
vllm/v1/attention/backends/ 下有 flash_attn、flashinfer、triton、flex_attention、composite、cpu_attn 等十余个实现。原因:
后端在启动时根据「模型 + 硬件 + 配置」自动选取,接口统一。你在 nano-vllm 里看到的 flash_attn_varlen_func / flash_attn_with_kvcache 两种调用,就是 FlashAttention backend 内部的两种路径。
vllm/config/compilation.py · L130–140class CUDAGraphMode(enum.Enum):
NONE = 0
PIECEWISE = 1
FULL = 2
FULL_DECODE_ONLY = (FULL, NONE)
FULL_AND_PIECEWISE = (FULL, PIECEWISE)
最后两个是「组合模式」,元组的第一位用于 decode 批次,第二位用于 mixed 批次(即含 prefill 的批次)[compilation.py L142–152]。
| 模式 | 怎么录 | 适用 |
|---|---|---|
NONE | 不录,纯 eager | 调试、不支持的算子 |
PIECEWISE | 把模型切成若干「静态片段」,每段各录一张图;attention 等动态算子留在图外 | prefill / mixed 批次 |
FULL | 完整model forward作为图单位 | backend支持的decode或prefill/mixed桶 |
FULL_DECODE_ONLY | decode 用 FULL,mixed 用 NONE | 保守配置 |
FULL_AND_PIECEWISE | decode 用 FULL,mixed 用 PIECEWISE | 一种混合策略,收益需测量 |
nano-vllm 的 prefill 每轮 shape 都变(token 总数不定),所以只能 eager。vLLM 的办法是 分段:
[embedding] [norm] [qkv] [attention ✗ 动态] [o_proj] [mlp] [norm]
└────── 录成图 ──────┘ └─── eager ───┘ └── 录成图 ──┘
把 attention 切成图与图之间的「洞」,其余静态部分照录。这样即使总 token 数不同,每个片段的 shape 也可以是固定的(按 token 数分桶)。这就是 piecewise 的核心思想。
FULL捕获完整model forward;backend支持时也可以处理non-uniform prefill/mixed。PIECEWISE切分模型,动态部分可留在图外。二者都需遵守捕获形状和地址合约,通常结合分桶与padding。
nano-vllm capture只包含self.model,compute_logits在图外。此vLLM提交的常见GPUModelRunner路径同样在_model_forward返回后选取sample_hidden_states,再compute_logits。因此不能依据FULL名字断言logits一定入图;见实际forward边界。
nano-vllm将输入值拷入预分配graph缓冲;vLLM还通过attention metadata、backend能力与模式调度协调更多模型/设备路径。应逐个核对缓冲字段,而非依据张量个数推断性能。
| 缩写 | 切什么 | 通信 | vLLM 里的位置 |
|---|---|---|---|
| TP | 每层权重 | 每层 all-reduce | distributed/parallel_state.py |
| PP | 层 | 段边界传激活 | pipeline parallel 调度 |
| DP | 整个模型副本 | 无(除非 MoE) | DPEngineCoreProc |
| EP | MoE 专家 | all-to-all | MoE 层 + DeepEP 类通信库 |
注意 vLLM 的 DP 是在 EngineCore 进程级做的:每个 DP rank 有自己的调度器和 worker 组,前端把请求分发过去。nano-vllm 完全没有这一层——它只有 TP。
下面三个场景,你会选哪种 CUDAGraphMode?
① FULL_AND_PIECEWISE可作为候选配置,随后在固定负载上与其他支持模式对照;不能预先保证收益最大。
② NONE:图内无法断点,调试必须退回 eager。
③ 先查backend的FULL能力和本轮shape桶:支持时prefill也可FULL,否则评估PIECEWISE或NONE。用固定负载测量后选择。
「vLLM 的 CUDA Graph 怎么做的?」
「runtime有NONE、PIECEWISE、FULL;策略按batch形态分派模式。FULL不是仅decode,是否覆盖prefill/mixed看backend。按真实调用边界区分model forward、logits和采样,再检查捕获桶与padding。」详见29课。
可以问:「分段图的边界怎么定?」——和 torch.compile 的 graph break 是同一个问题,值得单独一节课,如果你要深入编译方向我可以加。