Different results for PPDocLayoutV3 on CPU and CUDA

用户在运行 PPDocLayoutV3 模型(通过 Hugging Face Transformers 库)时,发现模型在 CPU 和 CUDA 设备上输出结果不一致,具体表现为 _cached_generate_anchors 函数生成的 anchor 掩码存在差异。该函数在 transforme

Different results for PPDocLayoutV3 on CPU and CUDA

Different results for PPDocLayoutV3 on CPU and CUDA

快速结论:该问题是由于 PyTorch 在 CPU 和 CUDA 设备上对浮点数舍入行为存在未定义差异,导致 _cached_generate_anchors 函数生成的二值掩码(valid_mask)在边界像素上出现不一致。优先排查是否在特定尺寸(如 50×50)下触发,并可尝试使用 torch.linspace 替代 torch.arange 后归一化的构造方式作为修复方案。

问题场景

用户在运行 PPDocLayoutV3 模型(通过 Hugging Face Transformers 库)时,发现模型在 CPU 和 CUDA 设备上输出结果不一致,具体表现为 _cached_generate_anchors 函数生成的 anchor 掩码存在差异。该函数在 transformers 仓库中被重复定义了至少 7 次,适用于多种 DETR 风格的模型实现。

报错原文

ERROR: Different masks on CPU and GPU

(这是用户自行编写的检测脚本输出的错误信息,用于判断掩码是否一致)

原因分析

问题根源在于 PyTorch 在 CPU 和 CUDA 上对浮点数除法的舍入行为不一致,这是一个已知的 PyTorch 底层行为差异(参考 pytorch/pytorch #186618)。在 _cached_generate_anchors 函数中,通过 torch.arange(..., device=device).float() 生成坐标网格后,执行 grid_xy[..., 0] /= widthgrid_xy[..., 1] /= height 归一化时,边界像素(如索引 0 和 49)的浮点计算在 CPU 和 CUDA 上可能产生不同的截断或舍入,进而导致 valid_mask = ((anchors > eps) * (anchors < 1 - eps)) 在边界条件判断上产生差异。

对于 50×50 的特征图,观察到了 97 个不匹配的像素,集中在边界区域。该行为在多次运行中稳定复现,表明并非随机噪声。当前尚不清楚哪种设备的行为是设计意图——有意见认为 CPU 掩码因对称性更可能是正确行为,但未经原始模型验证。

⚠️ 注意:仅修复数值一致性(如调整容忍度 eps)而不了解原始模型的预期行为,可能只是将问题转移到其他地方。需要 PP-DocLayoutV3 的原作者或开发者提供在原始硬件上的基准输出(logits、valid_mask 等)以确认正确的实现。

环境排查

  • 确认 Python 版本
  • 确认 PyTorch 版本(已知存在底层浮点舍入差异,涉及 torch.arange + 浮点运算在 CPU/CUDA 上的行为)
  • 确认 CUDA 版本及显卡型号
  • 确认 transformers 库版本(PPDocLayoutV3 相关实现)
  • 确认是否在特定特征图尺寸(如 50×50)下复现,其他尺寸可能不触发此问题

解决步骤

以下步骤基于 Issue 讨论中提出的替代构造方案,可优先尝试:

  1. 将原始的 _cached_generate_anchors 实现中通过 torch.arange 生成坐标后除以宽高的方式,替换为直接使用 torch.linspace 构造归一化坐标网格,避免浮点除法引入的舍入差异。
  2. 替换后的核心代码示例(来自 Issue 讨论中的解决方案):
import torch

def _cached_generate_anchors(device):
    width = height = 50
    # 直接使用 linspace 构造归一化坐标,而不是 arange + 除法
    anchors = torch.stack(
        torch.meshgrid(
            torch.linspace(0.5 / height, 1. - 0.5 / height, height, device=device).float(),
            torch.linspace(0.5 / width, 1. - 0.5 / width, width, device=device).float(),
            indexing="ij",
        ), -1
    ).unsqueeze(0)
    eps = 1e-2
    valid_mask = ((anchors > eps) * (anchors < 1 - eps)).all(-1, keepdim=True)
    return valid_mask

注意:该修改已在 Issue 讨论中被验证为能在 CPU 和 CUDA 上生成一致的掩码,且与原始 CPU 行为一致。但该方案仅是数值一致性修复,尚需原始模型开发者确认这是否是 正确 的行为。

  • 如果该修改被确认正确,建议对所有包含 _cached_generate_anchors 的重复实现进行统一替换(仓库中至少 7 处重复)。
  • 验证方法

    运行 Issue 中提供的复现脚本,确认 CPU 和 CUDA 生成的 valid_mask.equal() 比较下不再输出 “ERROR: Different masks on CPU and GPU” 信息。同时建议提取 PPDocLayoutV3 模型的完整推理输出(如 logits、预测框等),对比修改前后的整体差异,确保修复未影响模型的核心预测行为(参考 Issue 中关于需要原始基准输出的讨论)。

    参考来源

    huggingface/transformers #46506

    GamsGo AI

    AI 工具推荐

    想把多个 AI 模型放在一个入口?

    GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

    了解 GamsGo AI

    推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

    celebrityanime
    celebrityanime
    文章: 14064

    发表回复

    您的邮箱地址不会被公开。 必填项已用 * 标注