Prhub

#49531 [Perf] DeepSeek-OCR-2 TTFT Optimize

原始 PR 作者 LiuLi1998 合并时间 2026-07-26 13:53 文件变更 1 提交数 6 评论 10 代码增减 +32 / -36

执行摘要

优化 DeepSeek-OCR-2 注意力掩码计算,TTFT 降 46%

create_custom_4d_mask函数在CPU上循环构造掩码,是TTFT的瓶颈。PR body中指明该函数是CPU-bound,torch profiler显示aten::_index_put_impl_调用达229,296次,消耗52.2%的CPU时间。优化目标是将这一计算移至GPU并消除冗余索引操作。

值得精读。该PR清晰展示了如何通过分析数据模式(批次不变性)消除冗余CPU→GPU操作,并利用@lru_cache以极小成本实现加速。设计决策中关于缓存key范围的取舍(保留dtype和device)体现了对实际部署场景的考量。

讨论亮点

Review中,Isotr0py对缓存有效性提出质疑,担心sequence_length变化导致缓存低效;LiuLi1998回应实际只有288和512两个值,缓存有界。随后Isotr0py建议改用@classmethod+@lru_cache,并提交commit实现。最终方案保留了dtype和device作为缓存key,以兼容不同配置。

实现拆解

  1. 移除实例状态:删除CustomQwen2ModelInner.forward中的_current_token_type_ids实例变量,不再传递token_type_ids,因为mask生成不再依赖每个样本的token_type_ids。
  2. 新增类方法compute_mask_base:在CustomQwen2ModelInner中声明类方法,使用@classmethod@lru_cache(maxsize=8)缓存。该方法根据固定模式(前一半image token、后一半text token)在GPU上一次性计算[1,1,S,S]掩码基座。
  3. 简化_create_custom_4d_mask:原方法遍历batch、逐位置填充mask。改为调用compute_mask_base获得基座,然后通过expand(batch_size,-1,-1,-1)广播到batch维度。
  4. 调整调用链_update_causal_mask不再传递token_type_ids,直接调用_create_custom_4d_mask并处理padding mask。
  5. 性能验证:未添加单元测试,但PR中附带了详细的TTFT基准脚本和分析,证明优化效果。
文件 模块 状态 重要度
vllm/model_executor/models/deepencoder2.py 视觉编码 modified 7.45

关键符号

compute_mask_base _create_custom_4d_mask _update_causal_mask

关键源码片段

vllm/model_executor/models/deepencoder2.py core-logic

核心性能优化文件,重构了掩码计算和缓存逻辑

from functools import lru_cache
import torch# ... 在 CustomQwen2ModelInner 类中 ...@classmethod
@lru_cache(maxsize=8)
def compute_mask_base(cls, sequence_length: int, dtype: torch.dtype, device: torch.device):
    """
    Compute the base 4D attention mask for DeepSeek-OCR-2.
    The mask is batch-invariant because token_type_ids follows a fixed pattern:
    first half of tokens are image tokens (non-causal), second half are text tokens (causal).
    This method computes a single [1, 1, S, S] mask and relies on expand()
    in the caller to broadcast over batch dimension.
    """
    min_dtype = torch.finfo(dtype).min
    n_query = sequence_length // 2 # image token count (also text token count)
    # Indices of image tokens (first half)
    img = torch.arange(sequence_length, device=device) < n_query
    txt = ~img
    # Standard causal mask: lower triangular
    causal = torch.tril(torch.ones(sequence_length, sequence_length,
                                    dtype=torch.bool, device=device))
    # Allow: image token attends to all; text token attends to all image tokens
    # plus causal among text tokens.
    allow = img[None, :] | (txt[:, None] & txt[None, :] & causal)
    return torch.where(
        allow,
        torch.zeros((), dtype=dtype, device=device),
        torch.full((), min_dtype, dtype=dtype, device=device),
    )[None, None] # add head and batch dimsdef _create_custom_4d_mask(self, sequence_length, dtype, device, batch_size):
    """Compute the full mask by expanding the cached base to batch size."""
    base = self.compute_mask_base(sequence_length, dtype, device)
    return base.expand(batch_size, -1, -1, -1)

评论区精华

缓存 key 范围和实现方式 设计

Isotr0py 担心 sequence_length 变化导致缓存低效,建议 sequence_length 作为唯一 key;LiuLi1998 回应实际只有 288 和 512 两个值,缓存有界。随后 Isotr0py 建议改用 @classmethod+@lru_cache 实现缓存。

结论:保留 (sequence_length, dtype, device) 作为缓存 key,采用 @lru_cache,LiuLi1998 测试通过。 · 已解决

批次不变性假设验证 正确性

LiuLi1998 指出 token_type_ids 模式固定(前一半 image 后一半 text),因此 mask 可批次不变。

结论:通过基准测试和 profile 确认正确性。 · 已解决

风险与影响

风险较低。缓存key包括(sequence_length, dtype, device),典型部署下组合有限;lru_cache的maxsize=8限制了条目数。无动态控制流变化,正确性通过基准测试验证。未修改batch维度的计算,扩展不会引入新回归。若未来模型变种导致更多sequence_length值,需注意缓存命中率,但当前场景无虞。

直接影响使用DeepSeek-OCR-2模型的用户,TTFT显著降低(p50降46%),吞吐量提升83%。对其他模型无影响。团队维护成本极低——仅一个文件改动,无新增依赖,部署无需额外配置。

缓存假设依赖固定 token_type_ids 模式 仅针对 OCR-2 模型优化

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论