已关闭
[Bug]: npu_benchmark中level 1的31_IOU的 `torch_npu.npu_iou` 和docstring中PyTorch数据精度无法对齐 #5
Xiangyu Chang创建于  5月13日关闭于  6月1日
Xiangyu Chang
Xiangyu Chang
5月13日 创建

在提交新问题之前,请确保您已经在社区中搜索过相关问题,并使用了社区中提供的资源/工具后,仍未找到满意的解决方式。

⚠️ 安全信息提醒:请仔细检查提供的文本内容,确保其不包含敏感数据信息,包括但不限于:

  • API 令牌或密钥
  • 密码或身份验证凭证
  • 私有网址或接口地址
  • 个人或机密数据
  • ...

在分享配置信息或代码示例时,请将敏感信息脱敏处理,或使用 <TOKEN> 等占位符替代原有内容。

环境信息

Device Ascend 910B3
CANN 25.3.rc1
torch 2.7.1+cpu
torch_npu 2.7.1
Python 3.10.0

🐛 问题描述

一、问题简述

https://gitcode.com/Ascend/ascendc-kernelgen-data/blob/master/npu_benchmark/level1/31_IOU.py

torch_npu.npu_iou 内部走 ACL "Iou" 自定义算子(CANN runtime 里的预编译二进制 kernel),
源码不在任何公开仓里。op-plugin 的 cpp wrapper 是开源的,但只是 dispatch 层,看不到
真正算 overlap / union / divide 的 AscendC 实现。

影响:

  • 下游写等价实现没法做 bit-level 对齐。
  • 即使按 op-plugin cpp 里推断的契约(输入 cast fp16、eps=0.01、输出 cast 回原 dtype)
    写出一个 Python 模拟版,相对 npu_iou 输出的 max_abs_err 仍然在 e-3 ~ e-2 量级,
    无法过 fp16 默认 rtol=1e-3, atol=1e-3 allclose 精度检查。

二、环境

Device Ascend 910B3
CANN 25.3.rc1
torch 2.7.1+cpu
torch_npu 2.7.1
Python 3.10.0

三、最小复现

import torch, torch_npu
torch.manual_seed(0)
torch_npu.npu.set_device(0)

def make_boxes(n, dtype):
    pts = torch.rand(n, 4) * 100
    x1 = torch.minimum(pts[:, 0], pts[:, 2])
    x2 = torch.maximum(pts[:, 0], pts[:, 2]) + 1.0
    y1 = torch.minimum(pts[:, 1], pts[:, 3])
    y2 = torch.maximum(pts[:, 1], pts[:, 3]) + 1.0
    return torch.stack([x1, y1, x2, y2], dim=1).to(dtype)

def torch_ref_iou(bboxes, gtboxes, mode=0, min=0.0):
    """Standard PyTorch IoU formula (fp32 internal)."""
    if mode not in [0, 1]:
        raise ValueError(f"mode must be 0 (IoU) or 1 (IoF), got {mode}")

    n = bboxes.shape[0]
    m = gtboxes.shape[0]

    bboxes_float = bboxes.float()
    gtboxes_float = gtboxes.float()

    lt = torch.max(bboxes_float[:, :2].unsqueeze(1), gtboxes_float[:, :2].unsqueeze(0))
    rb = torch.min(bboxes_float[:, 2:].unsqueeze(1), gtboxes_float[:, 2:].unsqueeze(0))

    wh = (rb - lt).clamp(min=0)
    inter = wh[:, :, 0] * wh[:, :, 1]

    area1 = (bboxes_float[:, 2] - bboxes_float[:, 0]) * (bboxes_float[:, 3] - bboxes_float[:, 1])
    area2 = (gtboxes_float[:, 2] - gtboxes_float[:, 0]) * (gtboxes_float[:, 3] - gtboxes_float[:, 1])

    if mode == 0:
        union = area1.unsqueeze(1) + area2.unsqueeze(0) - inter
        iou = inter / union.clamp(min=1e-10)
    else:
        iou = inter / area2.unsqueeze(0).clamp(min=1e-10)

    return iou.t().to(bboxes.dtype)

def torch_ref_npu_contract(bboxes, gtboxes, mode=0):
    """Mimic the contract inferred from op-plugin cpp:
    cast inputs to fp16, divide with eps=0.01, cast back."""
    b16 = bboxes.to(torch.float16)
    g16 = gtboxes.to(torch.float16)
    out = torch_ref_iou(b16, g16, mode, eps=0.01)
    return out.to(bboxes.dtype)

for shape, dtype in [
    ((1024, 4), torch.float16),
    ((256, 4),  torch.float16),
    ((1024, 4), torch.float32),
    ((512, 4),  torch.float32),
]:
    b = make_boxes(shape[0], dtype).npu()
    g = make_boxes(shape[0] // 2, dtype).npu()
    out_npu = torch_npu.npu_iou(b, g, 0)
    out_std = torch_ref_iou(b.float(), g.float(), 0, eps=0).to(dtype)
    out_ctr = torch_ref_npu_contract(b, g, 0)
    e_std = (out_npu.float() - out_std.float()).abs()
    e_ctr = (out_npu.float() - out_ctr.float()).abs()
    print(f"dtype={dtype} shape={shape}:")
    print(f"  vs PyTorch (fp32, eps=0):       max={e_std.max():.3e} mean={e_std.mean():.3e} "
          f"allclose@1e-3={torch.allclose(out_npu, out_std, rtol=1e-3, atol=1e-3)}")
    print(f"  vs fp16+eps=0.01 contract:      max={e_ctr.max():.3e} mean={e_ctr.mean():.3e} "
          f"allclose@1e-3={torch.allclose(out_npu, out_ctr, rtol=1e-3, atol=1e-3)}")

实测输出(910B3 / CANN 25.3.rc1 / torch_npu 2.7.1):

dtype=float16 shape=(1024, 4):
  vs PyTorch (fp32, eps=1e-10):       max=6.348e-03 mean=4.811e-05 allclose@1e-3=False
  vs fp16+eps=0.01 contract:      max=6.348e-03 mean=5.190e-05 allclose@1e-3=False
dtype=float16 shape=(256, 4):
  vs PyTorch (fp32, eps=1e-10):       max=3.906e-03 mean=4.596e-05 allclose@1e-3=False
  vs fp16+eps=0.01 contract:      max=3.418e-03 mean=4.947e-05 allclose@1e-3=False
dtype=float32 shape=(1024, 4):
  vs PyTorch (fp32, eps=1e-10):       max=1.572e-02 mean=9.235e-05 allclose@1e-3=False
  vs fp16+eps=0.01 contract:      max=4.639e-03 mean=4.960e-05 allclose@1e-3=False
dtype=float32 shape=(512, 4):
  vs PyTorch (fp32, eps=1e-10):       max=4.239e-03 mean=9.143e-05 allclose@1e-3=False
  vs fp16+eps=0.01 contract:      max=1.953e-03 mean=5.102e-05 allclose@1e-3=False

四、定位过程

op-pluginhttps://github.com/Ascend/op-plugin/blob/master/op_plugin/ops/aclops/IouKernelNpu.cpp ) 里的 wrapper(acl_op::npu_iou)能看到的全部细节:

at::Tensor bboxes_fp16 = bboxes;
if (bboxes.scalar_type() != at::ScalarType::Half) {
    bboxes_fp16 = at_npu::native::custom_ops::_npu_dtype_cast(bboxes, at::kHalf);
}
// ...同样把 gtboxes cast 成 fp16...

at_npu::native::OpCommand cmd;
cmd.Name("Iou")
    .Input(bboxes_fp16)
    .Input(gtboxes_fp16)
    .Output(overlap)
    .Attr("mode", mode_str)
    .Attr("eps", static_cast<float>(0.01))
    .Run();

if (overlap.scalar_type() != bboxes.scalar_type()) {
    overlap = at_npu::native::custom_ops::_npu_dtype_cast(overlap, bboxes.scalar_type());
}

可见的契约(上面 Python torch_ref_npu_contract 就是按这套写的):

  • 输入 cast 到 fp16
  • eps = 0.01 加在分母(位置和写法看不到)
  • 输出按 fp16 算好再 cast 回输入 dtype

看不到的契约:

  • Iou ACL op 内部的 op 序列(先算 overlap 再 divide?还是 fused?fma 还是分立 mul+add?)
  • 分母里 eps 加在哪:area_union + eps 还是 max(area_union, eps) 还是 clamp?
  • fp16 reciprocal 用的是 HW 的 NR 近似还是其他实现,ULP 边界是多少
  • 累加器精度(fp16 还是中间走 fp32)
  • 输出 round-mode

五、影响

  • 任何参考pytorch docstring 的 pure-triton 实现都无法过默认 fp16 allclose。即使按上面的契约逐字段对齐,
    HW 内部序列差异仍贡献 e-3 量级误差。
  • 业务侧要替换 / 加速 npu_iou 时拿不到稳定的对齐基线,只能改 ref 或宽松 atol/rtol,
    失去和原 npu_iou 的语义等价性。
  • 跨硬件回归(NPU vs CPU baseline)也碰同样问题。

欢迎加入社区,感谢您对社区的贡献 🎉!

likedislike
Xiangyu ChangXiangyu Chang
5月13日 修改了issue 的描述
Xiangyu ChangXiangyu Chang
5月13日 修改了issue 的描述
Xiangyu ChangXiangyu Chang
5月13日 修改了issue 的描述
Xiangyu ChangXiangyu Chang
5月13日 修改了issue 的描述
rxtfeng成员
5月14日 评论:

/label add triaged

likedislike
ascend-robotascend-robot成员
5月14日 添加了label:triaged
rxtfeng成员
5月14日 评论:

/label add triaged

likedislike
zhaolinlin成员
6月1日 评论:

请参照最新精度标准测试,fp16 atol采用9e-2本地验证通过~
https://github.com/ElleElleWu/AscendOpGenAgent/blob/main/skills/triton/kernel-verifier/SKILL.md

likedislike
Zzhaolinlin成员
6月1日 issue状态由 TODO 改变为 DONE
Zzhaolinlin成员
6月1日 关闭了 issue