Closed
[Bug-Report|缺陷反馈]: aclnnArange out 元素个数少于推导长度时返回 0 并静默截断(out=16、推导 17,末值 15.5 丢失) #3972
liuhanwen8Add on  16 days agoClosed at  4 days ago
liuhanwen8
liuhanwen8
16 days ago Create

现象

aclnnArange 在 out 的元素个数小于 (start,end,step) 推导长度时,两段接口与同步全部返回 0,只写满 out 声明的元素个数,剩余值静默丢弃。

用例 start=-0.5, end=16, step=1,推导长度 ceil((16-(-0.5))/1)=17,值应为 -0.5,0.5,...,15.5:

组 out 长度 first/second/sync 实际写入 最后一个值 out 之后 64B 哨兵区
合法对照 17 0/0/0 17/17 15.5 未变
缺陷组 16 0/0/0 16/16 14.5(第 17 个值 15.5 丢失) 未变(无越界写)
反向用例 18 0/0/0 17/18 第 18 个元素保持哨兵 NaN(未被写) 未变

实测输出(每行一次独立进程):

out=17 derived=17 first=0 second=0 sync=0 written=17/17 tail_changed=0/64 value[0]=-0.500000 value[last]=15.500000
out=16 derived=17 first=0 second=0 sync=0 written=16/16 tail_changed=0/64 value[0]=-0.500000 value[last]=14.500000
out=18 derived=17 first=0 second=0 sync=0 written=17/18 tail_changed=0/64 value[0]=-0.500000 value[last]=nan

期望

资料 math/range/docs/aclnnArange.md 写明「从 start 起始到 end 结束按照 step 的间隔获取值,并保存到输出的 1 维张量,其中数据范围为 [start,end)」,调用示例按 ceil((end-start)/step) 分配 out。本用例应得到 17 个值。

out 元素个数与推导长度不一致时,接口应返回 ACLNN_ERR_PARAM_INVALID(161002),而不是返回 0 后只写一部分。

环境

  • Atlas A2 训练系列产品(910B2),CANN 9.2.0
  • 仓库 cann/ops-math,master 584b2ea6cbaeba684a51a74296d82d4d50ab3b0f
  • math/range/op_api/aclnn_arange.cpp:CheckParams 只校验空指针、dtype、step 符号,未校验 out 元素个数
  • math/range/op_host/arch35/range_tiling.cpp:204-207:推导长度与 out 元素个数不一致时只打 warning,并取两者较小值作为实际输出长度

复现步骤

  1. 保存下方 repro.cpp,执行 source $ASCEND_HOME_PATH/set_env.sh,确认 ASCEND_OPP_PATH=$ASCEND_HOME_PATH/opp。
  2. 编译:g++ -std=c++17 -O2 repro.cpp -o repro -I$ASCEND_HOME_PATH/include -I$ASCEND_HOME_PATH/include/aclnnop -L$ASCEND_HOME_PATH/lib64 -Wl,--allow-shlib-undefined -lascendcl -lopapi -lnnopbase
  3. 合法对照:ASCEND_RT_VISIBLE_DEVICES=<空闲卡> ./repro 17,输出 written=17/17 ... value[last]=15.500000。
  4. 缺陷组:ASCEND_RT_VISIBLE_DEVICES=<空闲卡> ./repro 16,输出 first=0 second=0 sync=0 written=16/16 ... value[last]=14.500000,第 17 个值 15.5 不在结果中。
  5. 反向用例:./repro 18,输出 written=17/18 ... value[last]=nan,第 18 个元素未被写。

复现脚本

repro.cpp(单文件,自包含,对照组与缺陷组共用同一进程代码,靠 argv 切换 out 长度):

// aclnnArange: out 元素个数与 (start,end,step) 推导长度不一致时的行为复现
// build: g++ -std=c++17 -O2 repro.cpp -o repro -I$ASCEND_HOME_PATH/include \
//   -I$ASCEND_HOME_PATH/include/aclnnop -L$ASCEND_HOME_PATH/lib64 \
//   -Wl,--allow-shlib-undefined -lascendcl -lopapi -lnnopbase
// run:   ./repro 17   # 合法对照(out == ceil((16-(-0.5))/1) == 17)
//        ./repro 16   # 缺陷组(out 少 1,第 17 个值 15.5 无处存放)
//        ./repro 18   # out 多 1,观察第 18 个元素是否被写
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <cmath>
#include <vector>
#include "acl/acl.h"
#include "aclnnop/aclnn_arange.h"

static uint32_t CA = 0x7FC00001u, CB = 0x7FC00002u;   // out 区 / 尾随区哨兵(NaN,可区分)

int main(int argc, char** argv)
{
    int64_t n = argc > 1 ? atoll(argv[1]) : 17;
    const size_t ob = (size_t)n * 4, tb = 64, total = ob + tb;  // out 之后留 64B 尾随哨兵区
    std::vector<uint8_t> host(total, 0), got(total, 0);
    for (int64_t i = 0; i < n; ++i) memcpy(host.data() + i * 4, &CA, 4);
    for (size_t i = 0; i < tb; i += 4) memcpy(host.data() + ob + i, &CB, 4);

    aclInit(nullptr); aclrtSetDevice(0);
    aclrtStream st = nullptr; aclrtCreateStream(&st);
    void *dev = nullptr, *ws = nullptr;
    aclrtMalloc(&dev, total, ACL_MEM_MALLOC_HUGE_FIRST);
    aclrtMemcpy(dev, total, host.data(), total, ACL_MEMCPY_HOST_TO_DEVICE);
    int64_t sh[1] = {n}, sd[1] = {1};
    aclTensor* out = aclCreateTensor(sh, 1, ACL_FLOAT, sd, 0, ACL_FORMAT_ND, sh, 1, dev);
    float start = -0.5f, end = 16.0f, step = 1.0f;
    aclScalar *s = aclCreateScalar(&start, ACL_FLOAT), *e = aclCreateScalar(&end, ACL_FLOAT);
    aclScalar *p = aclCreateScalar(&step, ACL_FLOAT);

    uint64_t wss = 0; aclOpExecutor* ex = nullptr;
    aclnnStatus r1 = aclnnArangeGetWorkspaceSize(s, e, p, out, &wss, &ex);
    aclnnStatus r2 = r1, r3 = r1;
    if (r1 == ACL_SUCCESS) {
        if (wss > 0) aclrtMalloc(&ws, wss, ACL_MEM_MALLOC_HUGE_FIRST);
        r2 = aclnnArange(ws, wss, ex, st);
        r3 = aclrtSynchronizeStream(st);
    }
    aclrtMemcpy(got.data(), total, dev, total, ACL_MEMCPY_DEVICE_TO_HOST);

    long written = 0, tail = 0;
    for (int64_t i = 0; i < n; ++i) { uint32_t v; memcpy(&v, got.data() + i * 4, 4); written += (v != CA); }
    for (size_t i = 0; i < tb; ++i) tail += (got[ob + i] != ((const uint8_t*)&CB)[i % 4]);
    float f0 = 0, fl = 0;
    memcpy(&f0, got.data(), 4);
    if (n > 0) memcpy(&fl, got.data() + (n - 1) * 4, 4);
    printf("out=%lld derived=%d first=%d second=%d sync=%d written=%ld/%lld tail_changed=%ld/%zu "
           "value[0]=%.6f value[last]=%.6f\n",
           (long long)n, (int)std::ceil((end - start) / step), (int)r1, (int)r2, (int)r3,
           written, (long long)n, tail, tb, f0, fl);
    aclDestroyTensor(out); aclDestroyScalar(s); aclDestroyScalar(e); aclDestroyScalar(p);
    if (ws) aclrtFree(ws);
    aclrtFree(dev); aclrtDestroyStream(st); aclrtResetDevice(0); aclFinalize();
    return 0;
}

影响

  • 按资料「约束说明」的提示,out 长度若按 float 计算、tiling 侧按 double 推导,两者结果会不同:out 偏小时尾部值静默缺失,out 偏大时尾部元素不被写并保留内存残留内容。
  • 上述两种方向都返回 0,业务侧从返回码与返回的 aclTensor 上都看不出异常,只有打开 plog(range_tiling.cpp 的 warning)才能看到 OFF is 16, but OFT is 17 一类记录。
  • 下游拿到与 arange 语义不一致的张量(少一个元素或多个未定义元素)继续参与后续计算,属于静默错误结果,按静默脏数据定级 P0。
  • 查重:同仓未见 aclnnArange 的同现象 issue;#3952(aclnnMaskedSelect 未写尾部语义未声明)属同类「out 尾部未定义」但接口与表现不同。

建议

  1. aclnnArangeGetWorkspaceSize 的 CheckParams 增加 out 元素个数校验:out_size == ceil((end-start)/step)(int64 用 CeilDiv,浮点用 double 推导),不一致返回 ACLNN_ERR_PARAM_INVALID(161002)。
  2. 资料错误码表补充该场景的说明(当前 161002 只列了 dtype 与 start/end/step 关系两类)。
  3. 兼容性取舍:若保留 out 偏大时只写推导长度的行为,需要把剩余元素显式写满并在资料中声明;out 偏小时必须返回错误码,不能返回 0。
likedislike
liuhanwen8liuhanwen8
16 days ago Added label: bug-report
陈思
陈思Collaborators
15 days ago Comments:

您好,感谢反馈!

问题分析

本例 start=-0.5、end=16、step=1 应生成 17 个元素,末项为 15.5;这三个数均可精确表示,不涉及临界浮点舍入导致的长度分歧。报告中 out=16 时只得到前 16 项,即使没有越界写,也没有完整表达该序列。接口语义。

在 cann/ops-math@584b2ea6 中,前端确实只检查指针、类型和步长关系,没有比较 out 元素数与推导长度;L0 临时结果又直接使用 out 的 shape,因此后续转换、拷贝没有通过独立推导形状发现这一差异。前端检查、结果分配。

您引用的 arch35 代码确实会告警后取较小长度,但当前公开构建中该部分未列 A2,不能直接认定它就是本次 910B2 的执行位置;A2 运行包内具体如何决定写入长度仍需核实。arch35 处理、构建范围。

处理方向

一个可行方向是在生成任务前,用与实际执行口径一致的长度推导核对 out,避免容量不足时静默丢弃序列;严格等长、不一致返回 161002 是可评估方案,并需同步说明约束和错误码。资料已有 float 输入长度计算及 double 差异告警的提示,收紧检查时需一起处理这部分兼容行为,不能简单把全部浮点长度改用 double。现有精度提示。INT64 则需保留整数精度,并避免差值和向上取整计算溢出。

若保留 out 偏大的兼容方式,还需明确有效长度、逻辑 shape 和尾部语义;仅将多余元素填 0,并不能让它们自动成为 Arange 序列的一部分。相关方案应保留半开区间、正负步长、空序列和非连续输出语义。我们会继续确认长度校验与既有兼容规则的对齐方式。

likedislike
陈思陈思Collaborators
15 days ago Assign chen_0715 as responsible person
陈思陈思Collaborators
15 days ago Added label: pending-confirmation
陈思陈思Collaborators
15 days ago Removed label: pending-confirmation
baijinxiang_hwCollaborators
13 days ago Comments:

/assign @doufloat

likedislike
CANN-robotCANN-robotCollaborators
13 days ago Assign doufloat as responsible person
Bbaijinxiang_hwCollaborators
13 days ago Associated pull request
CchenyifengCollaborators
12 days ago Removed responsible person chen_0715
liuhanwen8
liuhanwen8
12 days ago Comments:

补充一条同根因、不同接口的实测证据:aclnnRange(闭区间语义,floor((end-start)/step)+1)在这个问题上与 aclnnArange(半开区间,ceil)表现一致,但它的复现用例与 #3972 原文不同,建议修复时一并覆盖。

实测(Atlas A2 910B2,CANN 9.2.0,ops-math master 1b415030)

start=-0.5, end=16, step=1 → 推导长度 floor(16.5/1)+1 = 17;out 分别声明 17/16/18/0:

out 长度 第一段 第二段 实际写入 尾随 64B 哨兵
17(对照) 0 0 17/17 未变
16(缺陷) 0 0 16/16,第 17 个值 15.5 丢失 未变(无越界写)
18 0 0 17/18,第 18 个保持原值 未变
0(空) 0 0 0/0 未变

即:与 aclnnArange 完全同构 —— 只告警、取 min、返回 0、不越界。

与 aclnnArange 的差异(修复时需注意)

项 aclnnArange aclnnRange
区间语义 半开 [start, end) 闭 [start, end]
长度公式 ceil((end-start)/step) floor((end-start)/step)+1
长度推导代码 range_infershape.cpp:166-181(CeilDiv / std::ceil) range_infershape.cpp:158-160(整数除 + +1)

两者共用 op_api/arange.cpp 与 op_host/arch35/range_tiling.cpp,所以「aclnn 前端不校验 out 元素数」这一根因相同;但长度公式不同,若在校验里用同一个推导函数,需要按 isClosed 分支取值,否则会把合法的 aclnnRange 用例误判成越界。

复现

aclnnRange 单文件探针(自包含,DEV 见下表;Health=Alarm 的 die 上 aclrtSetDevice 会永久挂死):

// build: g++ -std=c++17 -O2 repro.cpp -o repro -I$ASCEND_HOME_PATH/include \
//   -I$ASCEND_HOME_PATH/include/aclnnop -L$ASCEND_HOME_PATH/lib64 \
//   -Wl,--allow-shlib-undefined -lascendcl -lopapi -lnnopbase
// run:   PROBE_DEV=<Health=OK 的卡号> ./repro
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
#include "acl/acl.h"
#include "aclnnop/aclnn_range.h"

int main()
{
    int n = 16;                                  // 改成 17 即合法对照
    aclInit(nullptr);
    aclrtSetDevice(getenv("PROBE_DEV") ? atoi(getenv("PROBE_DEV")) : 0);
    aclrtStream st = nullptr;
    aclrtCreateStream(&st);

    float sv = -0.5f, ev = 16.0f, stv = 1.0f;
    aclScalar* s = aclCreateScalar(&sv, ACL_FLOAT);
    aclScalar* e = aclCreateScalar(&ev, ACL_FLOAT);
    aclScalar* p = aclCreateScalar(&stv, ACL_FLOAT);

    size_t bytes = (size_t)n * 4;
    int64_t sh[1] = {n};
    void* dev = nullptr;
    aclrtMalloc(&dev, bytes + 64, ACL_MEM_MALLOC_HUGE_FIRST);
    std::vector<uint32_t> pre((bytes + 64) / 4, 0x7FC00001u);
    aclrtMemcpy(dev, bytes + 64, pre.data(), bytes + 64, ACL_MEMCPY_HOST_TO_DEVICE);
    aclTensor* out = aclCreateTensor(sh, 1, ACL_FLOAT, nullptr, 0, ACL_FORMAT_ND, sh, 1, dev);

    uint64_t ws = 0;
    aclOpExecutor* ex = nullptr;
    aclnnStatus r1 = aclnnRangeGetWorkspaceSize(s, e, p, out, &ws, &ex);
    printf("first=%d\n", (int)r1);
    if (r1 == 0) {
        printf("second=%d\n", (int)aclnnRange(nullptr, ws, ex, st));
        aclrtSynchronizeStream(st);
        std::vector<uint32_t> got((bytes + 64) / 4, 0);
        aclrtMemcpy(got.data(), bytes + 64, dev, bytes + 64, ACL_MEMCPY_DEVICE_TO_HOST);
        int written = 0, tail = 0;
        for (int i = 0; i < n; i++) { if (got[i] != 0x7FC00001u) { written++; } }
        for (size_t i = n; i < got.size(); i++) { if (got[i] != 0x7FC00001u) { tail += 4; } }
        printf("written=%d/%d tail_changed=%dB last=%.4f\n", written, n, tail,
               *reinterpret_cast<float*>(&got[n - 1]));
    }
    aclrtDestroyStream(st); aclrtFinalize();
    return 0;
}

实测输出:

# out=17(对照)
first=0
second=0
written=17/17 tail_changed=0B last=15.5000

# out=16(缺陷组)
first=0
second=0
written=16/16 tail_changed=0B last=14.5000

out=16 时 last=14.5、第 17 个值 15.5 完全不在结果里,两段与同步全返回 0;尾随哨兵 0 字节变化,确认不是越界写,是静默丢数据。

附带发现(同接口、另一条独立缺陷,已单独提 #4257)

aclnnRange 的 CheckParamsLogic 只校验 dim_num != 0,而告警文案写的是 Expected out to be a 1D tensor —— 2/3 维 out 被直接放行(两段均返回 0)。该问题与元素个数校验同属「aclnn 前端缺出参契约校验」,但可分开修,已在 #4257 单列。

likedislike
陈思
陈思Collaborators
11 days ago Comments:

谢谢补充 aclnnRange 的对照结果。这两条接口都有需要核对的输出长度校验缺口,但长度公式必须保留各自的区间语义。

问题分析

aclnnRange 按闭区间计算 floor((end-start)/step)+1,aclnnArange 按半开区间计算 ceil((end-start)/step)。您这组 start=-0.5、end=16、step=1 在两种语义下恰好都需要 17 个元素;步长恰好到达终点时则不同,例如 0、4、1 分别需要 5 个和 4 个元素,不能直接共用半开区间校验。Range 接口定义、长度推导分支。

核查 cann/ops-math@ae087b184,aclnnRange 前端没有比较 out 元素数与推导长度,随后以 isClosed=true 调用公共 L0;L0 临时结果直接沿用 out 的 shape。这些代码与您补充的 1b415030、原文的 584b2ea6 一致,支持两接口存在同类前端校验缺口。您报告的 out=16 丢失末项、out=18 尾部保持原值以及 out=0 返回成功,均应按“应生成 17 项”核对;哨兵未变说明该组实测没有观察到尾随区越界写,不能据此认为结果完整。Range 前端及调用、L0 分配。

您引用的 range_tiling.cpp 确有告警后取较小长度的实现;但该目录当前公开构建未列出 A2,仍不能把它直接认定为本次 910B2 安装包的实际执行位置。这里确认的是前端校验缺口,运行结果依据您提供的实测。tiling 分支、构建范围。

处理方向

关联 PR !6770 目前仍开放、尚未合入。核查其 5cfe6f0f 版本,改动是在 aclnnArange 前端新增长度检查:对原文这组样例,out=16/18 在第一段返回 161002,out=17 保留合法路径;同时考虑 INT64 向上取整和浮点两种计算口径的兼容。目前修改范围是 aclnnArange,尚不包含本次补充的 aclnnRange。

对 Range,一个可行方向是在第一段按闭区间长度核对 out,并保留正负步长、端点恰好命中、浮点精度和 INT64 溢出边界;这组输入的 out=0 也不能作为合法空序列放行。维数校验按您已单列的 #4257 分开跟踪。我们会继续跟踪两接口的覆盖范围。

likedislike
CANN-robotCANN-robotCollaborators
4 days ago Close issue
CANN-robotCANN-robotCollaborators
4 days ago Added label: resolved