Closed
[Bug-Report|缺陷反馈]: aclnnDiv/aclnnInplaceDiv 整型除法走 FP32 计算,INT32/INT64 大数结果错(x/1 都不等于 x) #3859
liuhanwen8Add on  18 days agoClosed at  12 days ago
liuhanwen8
liuhanwen8
18 days ago Create

问题描述

aclnnDiv(以及共用同一实现路径的 aclnnInplaceDiv)在 Atlas A2(910B2,CANN 9.2.0) 上,self/other/out 都是 INT32 或 INT64 时,结果按 FP32 计算后再截断,只要输入超过 2^24 就会错——连 x / 1 都返回错值。

INT32
    2147483647 / 1      实测=2147483647   期望=2147483647   OK
    1073741825 / 1      实测=1073741824   期望=1073741825   *** 错(/1 就错)
    2147483647 / 2      实测=1073741824   期望=1073741823   *** 错
    2000000001 / 3      实测=666666688    期望=666666667    *** 错
   -2147483647 / 2      实测=-1073741824  期望=-1073741823   *** 错

INT64
             4294967297 / 1          实测=4294967296            期望=4294967297            *** 错
    1152921504606846977 / 1          实测=1152921504606846976    期望=1152921504606846977   *** 错
    1000000000000000001 / 3          实测=333333339555495936    期望=333333333333333333    *** 错
    1234567890123456789 / 7          实测=176366853415763968    期望=176366841446208112    *** 错
       9007199254740993 / 1          实测=9007199254740992      期望=9007199254740993      *** 错
       1000000000000001 / 3          实测=333333317812224       期望=333333333333333       *** 错

(实测值就是 FP32 舍入后的结果,例如 4294967297 在 FP32 里就是 4294967296。)

定位(master efff3523)

math/div/op_api/aclnn_div.cpp:

op::DataType PromoteIntegerInputsToFloat(const op::DataType input)
{
    if (IsIntegralType(input)) {
        return op::DataType::DT_FLOAT;          // ← int32/int64 一律提升到 FP32
    }
    return input;
}

FP32 只有 24 位有效位,所以任何 |值| > 2^24 的整型输入都会丢位;INT64 场景误差可达 1e8 量级。

影响

  • 资料(math/div/docs/aclnnDiv&aclnnInplaceDiv.md)给出的公式是 out_i = input_i / other_i,参数表 self/other/out 都列了 INT32/INT64,没有任何整型精度损失的说明(对比 aclnnAdds 文档会明确写「int32/int64 的 other 大于 2^24 时可能存在精度损失」)。
  • 实际业务里 int64 除法常用来换算时间戳(ns → s)、ID 取模分组、计数归一化,量级普遍 > 2^24,属于静默算错,比报错更难发现;x / 1 都不等于 x 这一点最容易用来判定。

环境信息

  • 昇腾硬件型号:Atlas A2 训练系列(910B2)
  • CANN 版本:9.2.0
  • 操作系统:Ubuntu 22.04 / aarch64,g++ 11.4.0

重现步骤

  1. 保存下方 repro_div_int.cpp;
  2. source ${ASCEND_HOME_PATH}/set_env.sh;
  3. 编译运行:
g++ -O2 repro_div_int.cpp -o repro_div_int \
    -I${ASCEND_HOME_PATH}/include -L${ASCEND_HOME_PATH}/lib64 \
    -lascendcl -lnnopbase -lopapi
./repro_div_int            # aclnnDiv
./repro_div_int inplace    # aclnnInplaceDiv(同样错)

预期结果

INT32/INT64 的除法结果等于 C/C++ 的截断除法(a / b):

  • 1073741825 / 1 == 1073741825、4294967297 / 1 == 4294967297;
  • 2147483647 / 2 == 1073741823、2000000001 / 3 == 666666667;
  • 1000000000000000001 / 3 == 333333333333333333。

建议二选一:

  1. 补整型除法路径(推荐):INT32 用 S32 整型除法、INT64 用 S64 整除(或在 FP64 上算并保证 ≤2^53 之外也精确),不再经 FP32;
  2. 若确认产品口径就是「整型也按 FP32 算」,则必须在资料里写明精度边界(|值| > 2^24 结果不可预期),并明确 x/1 也可能不精确——但这与现有参数表把 INT64 列为支持相矛盾。
复现脚本 repro_div_int.cpp(仅依赖 CANN)
/**
 * aclnnDiv / aclnnInplaceDiv 整型除法精度复现(A2 910B2 / CANN 9.2.0)
 * 编译: g++ -O2 repro_div_int.cpp -o repro_div_int \
 *   -I${ASCEND_HOME_PATH}/include -L${ASCEND_HOME_PATH}/lib64 -lascendcl -lnnopbase -lopapi
 * 运行: ./repro_div_int [inplace]
 */
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <vector>
#include "acl/acl.h"
#include "aclnnop/aclnn_div.h"

#define CK(x) do { auto r_ = (x); if (r_ != 0) { printf("[FAIL] %s rc=%d\n", #x, (int)r_); return 1; } } while (0)

static aclTensor* mk(const std::vector<int64_t>& shape, void* dev, aclDataType dt) {
    std::vector<int64_t> st(shape.size(), 1);
    for (int64_t i = (int64_t)shape.size() - 2; i >= 0; --i) st[i] = shape[i + 1] * st[i + 1];
    return aclCreateTensor(shape.data(), shape.size(), dt, st.data(), 0, ACL_FORMAT_ND,
                           shape.data(), shape.size(), dev);
}

static aclrtStream g_stream;

static bool run_div(aclDataType dt, void* d_s, void* d_o, void* d_out,
                    int64_t n, void* got, bool inplace) {
    std::vector<int64_t> shape = {n};
    aclTensor* ts = mk(shape, d_s, dt);
    aclTensor* to = mk(shape, d_o, dt);
    aclTensor* tout = mk(shape, d_out, dt);
    uint64_t ws = 0;
    aclOpExecutor* ex = nullptr;
    aclnnStatus st = inplace ? aclnnInplaceDivGetWorkspaceSize(ts, to, &ws, &ex)
                             : aclnnDivGetWorkspaceSize(ts, to, tout, &ws, &ex);
    if (st != 0) { printf("  第一段 rc=%d\n", (int)st); aclDestroyTensor(ts); aclDestroyTensor(to); aclDestroyTensor(tout); return false; }
    void* wsp = nullptr;
    if (ws > 0) CK(aclrtMalloc(&wsp, ws, ACL_MEM_MALLOC_HUGE_FIRST));
    st = inplace ? aclnnInplaceDiv(wsp, ws, ex, g_stream) : aclnnDiv(wsp, ws, ex, g_stream);
    aclrtSynchronizeStream(g_stream);
    aclrtMemcpy(got, n * (dt == ACL_INT64 ? 8 : 4), inplace ? d_s : d_out,
                n * (dt == ACL_INT64 ? 8 : 4), ACL_MEMCPY_DEVICE_TO_HOST);
    if (wsp) aclrtFree(wsp);
    aclDestroyTensor(ts); aclDestroyTensor(to); aclDestroyTensor(tout);
    return st == 0;
}

int main(int argc, char** argv) {
    bool inplace = argc > 1 && strcmp(argv[1], "inplace") == 0;
    CK(aclInit(nullptr));
    CK(aclrtSetDevice(0));
    CK(aclrtCreateStream(&g_stream));

    const int64_t N = 8;
    int32_t s32[N] = {2147483647, 2147483647, 1073741825, 2000000001,
                      -2147483647, 123456789, 1000000007, 16777217};
    int32_t o32[N] = {1, 2, 1, 3, 2, 7, 1000000007, 2};
    int64_t s64[N] = {4294967297LL, 1152921504606846977LL, 1000000000000000001LL,
                      1234567890123456789LL, 9007199254740993LL, 1000000000000001LL,
                      17179869184LL, -4611686018427387904LL};
    int64_t o64[N] = {1, 1, 3, 7, 1, 3, 1000000000LL, 2};

    void *ds = nullptr, *do_ = nullptr, *dout = nullptr;
    const size_t b32 = N * 4, b64 = N * 8;
    CK(aclrtMalloc(&ds, b64, ACL_MEM_MALLOC_HUGE_FIRST));
    CK(aclrtMalloc(&do_, b64, ACL_MEM_MALLOC_HUGE_FIRST));
    CK(aclrtMalloc(&dout, b64, ACL_MEM_MALLOC_HUGE_FIRST));

    printf("==== %s ====\n", inplace ? "aclnnInplaceDiv" : "aclnnDiv");
    int32_t got32[N] = {0};
    CK(aclrtMemcpy(ds, b32, s32, b32, ACL_MEMCPY_HOST_TO_DEVICE));
    CK(aclrtMemcpy(do_, b32, o32, b32, ACL_MEMCPY_HOST_TO_DEVICE));
    if (run_div(ACL_INT32, ds, do_, dout, N, got32, inplace)) {
        printf("INT32:\n");
        for (int i = 0; i < N; ++i) {
            int64_t exp = (int64_t)s32[i] / (int64_t)o32[i];
            printf("  %12d / %-12d 实测=%-12d 期望=%-12lld %s\n", s32[i], o32[i],
                   got32[i], (long long)exp, (int64_t)got32[i] == exp ? "OK" : "*** 错");
        }
    }
    int64_t got64[N] = {0};
    CK(aclrtMemcpy(ds, b64, s64, b64, ACL_MEMCPY_HOST_TO_DEVICE));
    CK(aclrtMemcpy(do_, b64, o64, b64, ACL_MEMCPY_HOST_TO_DEVICE));
    if (run_div(ACL_INT64, ds, do_, dout, N, got64, inplace)) {
        printf("INT64:\n");
        for (int i = 0; i < N; ++i) {
            int64_t exp = s64[i] / o64[i];
            printf("  %21lld / %-10lld 实测=%-21lld 期望=%-21lld %s\n", (long long)s64[i],
                   (long long)o64[i], (long long)got64[i], (long long)exp,
                   got64[i] == exp ? "OK" : "*** 错");
        }
    }
    aclrtFree(ds); aclrtFree(do_); aclrtFree(dout);
    aclrtDestroyStream(g_stream);
    aclrtResetDevice(0);
    aclFinalize();
    return 0;
}
likedislike
陈思
陈思Collaborators
17 days ago Comments:

您好,感谢反馈!您提供的 x/1 用例反映了整数业务对精度的要求。这里需要分别看输入/输出类型支持、除法计算类型和舍入语义。

问题分析

本次核对的是 cann/ops-math 的 b19eaee4f58e5e1e3cb3b2ecbfed2d02c11c820a,不是报告中的 efff3523,以下为这一提交的代码结论,尚不能替代对 CANN 9.2.0 / 910B2 实测结果的确认。

在该提交中,Div 的非混合类型路径对 A2 的两路 INT64 输入仍推导为 FLOAT,随后先 Cast 两个输入、调用 RealDiv,再将结果 Cast 到指定的 out 类型。输入在转 FLOAT 时丢失的信息,最后转回 INT64 无法恢复,因此能够解释您列出的 4294967297/1 这类现象。A2 的两路 INT32 输入已有保留 INT32 的分支,不能再概括为所有整数输入都先转 FLOAT;但 RealDiv 的 mode=0 路径仍分配 FLOAT 结果,再转回 INT32,同样不能据此保证大整数商精确。

FP32 可精确表示所有绝对值不超过 2^24 的整数;超过后只可表示其中一部分整数,并非每个超过该值的输入都会出错,也不能由此给出所有除法结果都精确的保证。aclnnInplaceDiv 对这类整数输入复用 Div 路径,结果写回 selfRef;它还要求广播后的 shape 与 selfRef 一致,并满足输出类型可转换条件,原地计算本身不会增加精度。

Div 文档列出 INT32/INT64,并要求满足类型推导及输出转换规则,但没有对 A2 说明上述整数精度边界。文档中“整数推导为 FLOAT、禁止整数输出”的说明限定于 Ascend 950,不能套用于 A2。类型表表示接口接受这些类型,单凭类型表和除法公式还不能认定接口承诺 C/C++ 的精确截断整除;不过,A2 的计算精度和结果转换行为确实需要更清楚的说明。

处理方向

您期望的 C/C++ 截断语义,更对应 aclnnDivMod/aclnnInplaceDivMod 的 mode=1:mode=0 不执行舍入,mode=1 向零舍入,mode=2 向下舍入。这里仍需区分“舍入方式”与“整型全范围精度”:当前 A2 的 INT64 trunc 路径也会先转 FLOAT,因此不能把切换 mode=1 当作您这些 INT64 大数用例的已验证解决方案。

一个可行的优化方向是在明确承诺精确截断整除的接口/模式中保留整数计算路径,避免中间经过 FP32,并保留混合类型推导、广播及原地写回约束,同时明确除零与最小负整数除以 -1 的边界。仅将 FP32 改为 FP64 也不能覆盖 INT64 全范围;FP64 只能保证不超过 2^53 的整数均可精确表示。若调整现有无 mode 的 Div,还需评估浮点商语义及既有调用的兼容影响,不能只按输出是整数就直接改变整个接口的计算语义。

另一项可行方向是在 A2 文档中明确实际计算类型、结果转换及大整数精度限制,并加入 x/1 的示例;文档澄清有助于选择接口,但不等于已经满足业务的精确整除需求。我们会继续确认该精度诉求与接口契约的关系,评估整数路径和文档说明的适用范围。

likedislike
陈思陈思Collaborators
17 days ago Assign chen_0715 as responsible person
陈思陈思Collaborators
17 days ago Added label: pending-confirmation
陈思陈思Collaborators
15 days ago Removed label: pending-confirmation
CchenyifengCollaborators
12 days ago Assign m0_55003149 as responsible person
CchenyifengCollaborators
12 days ago Removed responsible person chen_0715
weikeweikeCollaborators
12 days ago Associated pull request
weikeweikeCollaborators
12 days ago Associated pull request
weike
weikeCollaborators
12 days ago Comments:

暂时先补充资料,写明精度边界(|值| > 2^24 结果不可预期),并明确 x/1 也可能不精确

likedislike
weikeweikeCollaborators
12 days ago changed custom state from 进行中 to 已完成
weikeweikeCollaborators
12 days ago Close issue