执行摘要
- 一句话:优化 Qwen2.5-VL encoder CUDA graph 窗口序列上界,B200 性能提升 3x+
- 推荐动作:该 PR 值得精读,展示了在 CUDA graph replay 中处理变长输入的正确姿势,尤其是
padding_logics 设计模式体现了插件化思想。评审过程中对灵活性与显式性之间的权衡也值得关注。
功能与动机
PR #40830 启用 Qwen2.5-VL 的 encoder CUDA graph 后,在 B200 上使用默认的 FLASH_ATTN 后端出现严重性能退化,原因是窗口序列数上界估计过于保守,导致 FlashAttention 启动大量空计算块。同时 FlashInfer 后端使用不同的 packed cu_seqlens 布局,通用 padding 方式会写错向量偏移部分。
实现拆解
- 新增 padding 函数:在 qwen2_5_vl.py 中定义
_pad_cumulative_seqlens_buffer(通用)和 _pad_flashinfer_cu_seqlens_buffer(FlashInfer 专用)。后者将 packed 布局的 [Q/K/O offsets, V offsets] 拆为两段独立 padding。
- 新增窗口序列数上界计算:在 Qwen2_5_VLProcessor 中实现
get_encoder_cudagraph_max_window_seqs,基于视觉融合网格尺寸做几何推导,给出远紧于 token_budget 的上界,避免过度分配。
- 重构 replay 拷贝逻辑:在 encoder_cudagraph.py 的
_run_budget_graph 中,将硬编码的零填充+切片拷贝替换为通过 EncoderCudaGraphConfig.padding_logics 查找模型特定 padding 函数,默认使用 _copy_padded_buffer。
- 配置插件化:在 encoder_cudagraph_defs.py 的
EncoderCudaGraphConfig 中新增 padding_logics 字典字段,允许模型为每个 buffer key 注册自定义 padding 函数。
- 模型注册:在 qwen2_5_vl.py 的
get_encoder_cudagraph_config 中,根据 attention 后端类型注册对应的 padding 函数到 padding_logics。
此变更未新增测试文件,但通过已有 benchmark 验证性能。
关键文件:
vllm/model_executor/models/qwen2_5_vl.py(模块 视觉模型;类别 source;类型 core-logic;符号 _pad_cumulative_seqlens_buffer, _pad_flashinfer_cu_seqlens_buffer, get_encoder_cudagraph_max_window_seqs): 核心变更,包含两种 padding 函数和窗口序列数上界计算方法,是性能提升的关键。
vllm/v1/worker/encoder_cudagraph.py(模块 编码器CG管理;类别 source;类型 core-logic;符号 _copy_padded_buffer): 通用管理器中引入可插拔 padding 逻辑,通过 padding_logics 代理调用。
vllm/v1/worker/encoder_cudagraph_defs.py(模块 配置定义;类别 source;类型 dependency-wiring): 定义 EncoderCudaGraphPaddingLogic 类型和 padding_logics 配置字段,实现插件化。
关键符号:_pad_cumulative_seqlens_buffer, _pad_flashinfer_cu_seqlens_buffer, get_encoder_cudagraph_max_window_seqs, _copy_padded_buffer
关键源码片段
vllm/model_executor/models/qwen2_5_vl.py
核心变更,包含两种 padding 函数和窗口序列数上界计算方法,是性能提升的关键。
def _pad_cumulative_seqlens_buffer(
dst: torch.Tensor,
src: torch.Tensor,
) -> None:
# 通用 padding:将有效数据拷贝到 dst 头部,
# 尾部用最后一个有效值填充,使 CUDA graph replay 时
# padded 位置表示空序列(cu_seqlens 尾部重复)。
n = src.shape[0]
dst.zero_()
dst[:n].copy_(src)
if n < dst.shape[0]:
dst[n:] = src[-1]
def _pad_flashinfer_cu_seqlens_buffer(
dst: torch.Tensor,
src: torch.Tensor,
) -> None:
# FlashInfer 使用 packed 布局:[Q/K/O offsets][V offsets],
# 两段需要独立 padding。
src_mid = src.shape[0] // 2
dst_mid = dst.shape[0] // 2
assert src_mid <= dst_mid, (
f"FlashInfer cu_seqlens replay buffer is larger than capture buffer: "
f"src_section={src_mid}, dst_section={dst_mid}"
)
dst.zero_()
# 处理前半段(Q/K/O 偏移)
dst[:src_mid].copy_(src[:src_mid])
if src_mid < dst_mid:
dst[src_mid:dst_mid] = src[src_mid - 1]
# 处理后半段(V 偏移)
dst[dst_mid : dst_mid + src_mid].copy_(src[src_mid:])
if dst_mid + src_mid < dst.shape[0]:
dst[dst_mid + src_mid :] = src[-1]
def get_encoder_cudagraph_max_window_seqs(
self,
token_budget: int,
max_batch_size: int,
max_frames_per_batch: int,
) -> int:
# 基于几何形状计算 window 序列数上界,避免用 token_budget 直接作为序列数
# 导致过度分配。
vit_merger_window_size = (
self.window_size // self.spatial_merge_size // self.patch_size
)
max_sequence_units = max(max_batch_size, max_frames_per_batch)
# 最坏情况:一条细条只向一个方向延伸,所需窗口数为
# ceil(token_budget / window_side)
max_strip_windows = (
token_budget + vit_merger_window_size - 1
) // vit_merger_window_size
# 但窗口序列数不可能超过 token_budget,取 min 收紧上界
return min(token_budget, max_sequence_units + max_strip_windows)
vllm/v1/worker/encoder_cudagraph.py
通用管理器中引入可插拔 padding 逻辑,通过 padding_logics 代理调用。
@staticmethod
def _copy_padded_buffer(
dst: torch.Tensor,
src: torch.Tensor,
) -> None:
# 默认 padding:先清零,再拷贝有效数据。
dst.zero_()
dst[: src.shape[0]].copy_(src)
# 在 _run_budget_graph 中调用:
padding_logic = self.config.padding_logics.get(
key, self._copy_padded_buffer
)
padding_logic(buf, src)
评论区精华
风险与影响
关联脉络
- PR #42787 [MM][CG] Switch default MM encoder attention to FlashInfer on B200: 该 PR 将 B200 的默认 MM encoder attention 切换到 FlashInfer,两者配合解决性能退化。
- PR #40830 Enable encoder CUDA graph for Qwen2.5-VL: 首次启用 Qwen2.5-VL encoder CUDA graph,但引入 over-padding 问题。
参与讨论