Closed
[Bug-Report|缺陷反馈]: aclnnRepeat 在 repeats 含 0 或空 Tensor 时绕过 out 形状校验,两段均返回 0 且不写输出 #3996
liuhanwen8Add on  18 days agoClosed at  8 days ago
liuhanwen8
liuhanwen8
18 days ago Create

一、环境信息

项 值
硬件 Ascend 910B2(Atlas A2 训练系列)
CANN 版本 9.2.0
驱动/固件 npu-smi 26.1.1
操作系统 Linux 5.15.0-94-generic aarch64
算子仓 cann/ops-math(master)
算子目录 math/tile(910B 走 ops_legacy/tile 预编译内核)

二、问题描述

aclnnRepeat 在以下三种情形下会完全跳过 out 的形状校验,两段接口都返回 0,
并且一个元素都不写入 out:

  1. repeats 中含 0(输出必然含 0 元素)
  2. self 是空 Tensor
  3. out 是空 Tensor

此时即使把 out 声明成与 tile 语义完全无关的形状(例如 self=[3,2]、
repeats=[0,4]、out=[4,4]),接口仍返回 0 / 0,调用方拿到的是未初始化的显存内容。

资料 docs/aclnnRepeat.md 的错误码表声明「self 和 out 的数据类型和数据格式不在支持的范围内」
以及形状相关约束应返回 161002;正常路径(repeats 不含 0、非空 Tensor)确实返回 161002,
说明该校验存在但在提前返回分支被绕过。

三、复现步骤

  1. 准备一张 Ascend 910B2,安装 CANN 9.2.0,source $ASCEND_HOME_PATH/set_env.sh

  2. 保存下方复现代码为 repeat_repro.cpp

  3. 编译($ASCEND_HOME_PATH 为 CANN 安装路径):

    g++ -std=c++17 -O2 repeat_repro.cpp -o repeat_repro \
        -I$ASCEND_HOME_PATH/aarch64-linux/include \
        -L$ASCEND_HOME_PATH/aarch64-linux/lib64 \
        -lascendcl -lnnopbase -lopapi_math -lopapi
    
  4. 运行:

    export LD_LIBRARY_PATH=$ASCEND_HOME_PATH/aarch64-linux/lib64:$LD_LIBRARY_PATH
    ./repeat_repro
    

四、复现代码

#include <cstdio>
#include <cstdint>
#include <vector>
#include <cmath>
#include "acl/acl.h"
#include "aclnnop/aclnn_repeat.h"

static int64_t Size(const std::vector<int64_t>& s)
{
    int64_t n = 1;
    for (auto d : s) { n *= d; }
    return n;
}

// fill=true 填 1..n;false 填 NaN 金丝雀(用于判「输出是否被写过」)
static aclTensor* MakeTensor(const std::vector<int64_t>& sh, void** dev, bool fill)
{
    int64_t n = Size(sh);
    size_t bytes = static_cast<size_t>(n) * sizeof(float);
    if (bytes == 0) { bytes = 1; }
    if (aclrtMalloc(dev, bytes, ACL_MEM_MALLOC_HUGE_FIRST) != ACL_SUCCESS) { return nullptr; }
    std::vector<float> h(n > 0 ? n : 1, 0.0f);
    for (int64_t i = 0; i < n; ++i) { h[i] = fill ? static_cast<float>(i + 1) : std::nanf(""); }
    aclrtMemcpy(*dev, bytes, h.data(), bytes, ACL_MEMCPY_HOST_TO_DEVICE);
    std::vector<int64_t> st(sh.size(), 1);
    for (int64_t i = static_cast<int64_t>(sh.size()) - 2; i >= 0; --i) { st[i] = sh[i + 1] * st[i + 1]; }
    return aclCreateTensor(sh.data(), sh.size(), ACL_FLOAT, st.data(), 0,
                           ACL_FORMAT_ND, sh.data(), sh.size(), *dev);
}

static void Run(const std::vector<int64_t>& ss, const std::vector<int64_t>& rep,
                const std::vector<int64_t>& os, aclrtStream stream, const char* tag)
{
    void* sAddr = nullptr; void* oAddr = nullptr;
    aclTensor* self = MakeTensor(ss, &sAddr, true);
    aclTensor* out = MakeTensor(os, &oAddr, false);
    aclIntArray* repeats = aclCreateIntArray(rep.data(), rep.size());
    uint64_t ws = 0; aclOpExecutor* ex = nullptr;
    int r1 = static_cast<int>(aclnnRepeatGetWorkspaceSize(self, repeats, out, &ws, &ex));
    int r2 = -1;
    if (r1 == 0) {
        void* wsAddr = nullptr;
        if (ws > 0) { aclrtMalloc(&wsAddr, ws, ACL_MEM_MALLOC_HUGE_FIRST); }
        r2 = static_cast<int>(aclnnRepeat(wsAddr, ws, ex, stream));
        aclrtSynchronizeStream(stream);
        if (wsAddr != nullptr) { aclrtFree(wsAddr); }
    }
    int64_t n = Size(os); int nanCnt = -1;
    if (r1 == 0 && r2 == 0 && n > 0) {
        std::vector<float> h(n, 0.0f);
        size_t bytes = static_cast<size_t>(n) * sizeof(float);
        aclrtMemcpy(h.data(), bytes, oAddr, bytes, ACL_MEMCPY_DEVICE_TO_HOST);
        nanCnt = 0;
        for (int64_t i = 0; i < n; ++i) { if (std::isnan(h[i])) { ++nanCnt; } }
    }
    printf("%s ret1=%d ret2=%d nan_left=%d/%lld\n", tag, r1, r2, nanCnt,
           static_cast<long long>(n));
    aclDestroyTensor(self); aclDestroyTensor(out); aclDestroyIntArray(repeats);
    aclrtFree(sAddr); aclrtFree(oAddr);
}

int main()
{
    aclInit(nullptr); aclrtSetDevice(0);
    aclrtStream stream = nullptr; aclrtCreateStream(&stream);
    Run({3, 2}, {2, 4}, {6, 4}, stream, "[对照组 CTRL]");       // 正常路径 out 形状错
    Run({3, 2}, {2, 4}, {6, 8}, stream, "[对照组 CTRL-OK]");    // 正常路径 out 形状对
    Run({3, 2}, {0, 4}, {4, 4}, stream, "[缺陷组 DEFECT-1]");   // repeats 含 0
    Run({0, 2}, {2, 4}, {9, 9}, stream, "[缺陷组 DEFECT-2]");   // self 为空
    Run({3, 2}, {2, 4}, {0, 8}, stream, "[缺陷组 DEFECT-3]");   // out 为空
    aclrtDestroyStream(stream); aclrtResetDevice(0); aclFinalize();
    return 0;
}

五、实测输出

[对照组 CTRL] ret1=161002 ret2=-1 nan_left=-1/24
[对照组 CTRL-OK] ret1=0 ret2=0 nan_left=0/48
[缺陷组 DEFECT-1] ret1=0 ret2=0 nan_left=16/16
[缺陷组 DEFECT-2] ret1=0 ret2=0 nan_left=81/81
[缺陷组 DEFECT-3] ret1=0 ret2=0 nan_left=-1/0

对照说明:

  • CTRL:正常路径 out 形状写错 → 161002,证明形状校验在正常路径存在
  • CTRL-OK:正常路径形状正确 → 0/0,48 个元素全部写入(金丝雀 0/48 保留)
  • DEFECT-1/2:0/0 且金丝雀 16/16、81/81 全部保留 → 一个元素都没写
  • DEFECT-3:out 为空 tensor → 0/0

六、期望结果

DEFECT-1/2/3 三种构造的 out 形状均与 tile 语义推导结果不一致,
应返回 161002(ACLNN_ERR_PARAM_INVALID),与 CTRL 一致。

七、定位信息

math/tile/op_api/aclnn_repeat.cpp 的提前返回分支位于形状校验之前:

if (self->IsEmpty() || out->IsEmpty() || CheckRepeatsZero(repeats)) {
    *workspaceSize = 0;
    uniqueExecutor.ReleaseTo(executor);
    return ACLNN_SUCCESS;
}
// ... 后面才做:
auto repeatOut = l0op::Tile(selfUnsqueeze, repeats, uniqueExecutor.get());
CHECK_RET(CheckRepeatOutShape(repeatOut, out), ACLNN_ERR_PARAM_INVALID);

建议:把 out 的形状推导与校验提到提前返回之前(空 Tensor 与 repeats 含 0 的输出
形状是可静态推导的),或在提前返回分支内补一次形状断言。

八、影响面

静默脏数据。任何在 repeats 中带 0(例如按动态 batch 计算出的重复次数,padding 后为 0)
或传入空 Tensor 的调用方,都会在无错误码的情况下拿到未初始化显存作为结果,
下游表现为随机数值错误而非报错,排查成本高。

likedislike
liuhanwen8liuhanwen8
18 days ago Added label: bug-report
xujiachen8Collaborators
18 days ago Comments:

您好,感谢反馈!

问题分析

核对 cann/ops-math 提交 5f717513abcb6a6b94a4ae61a8151e4ba65e9eb3,第一段先校验指针、类型、repeats 长度/取值及维度,随后在 self 空、out 空或 repeats 含 0 时直接返回成功;输出形状比较却位于后面的 Tile 路径。因此这三种分支会绕过输出形状一致性检查,且不会添加 Tile 和结果写回任务,与您在 Ascend 910B2、CANN 9.2.0 上报告的结果相符。检查顺序与提前返回

例如 [3,2] 与 repeats [0,4] 应得到 [0,8],不能接受非空 out [4,4];[0,2] 与 [2,4] 应为 [0,8],也不能接受 [9,9]。前两组保留全部 NaN 金丝雀说明没有写回,而非算出了 NaN;out 本身为空时则没有可读取的逻辑元素,不能称为读取到了脏输出。本轮为源码核查,未重新上板验证。

处理方向

可考虑在提前返回前按 repeats 长度为 self 补齐前导 1、逐维计算期望形状并与 out 比较,不一致时返回 161002;合法空结果仍应保持成功和零工作量。这样无需为了校验而构建空数据计算任务,同时需要保留零维输入、扩维 repeat 及现有类型/维度检查。我们会继续跟踪这一提前返回分支的形状校验问题。

likedislike
Xxujiachen8Collaborators
18 days ago Assign chen_0715 as responsible person
Xxujiachen8Collaborators
18 days ago Added label: pending-confirmation
陈思陈思Collaborators
18 days ago Removed label: pending-confirmation
CchenyifengCollaborators
16 days ago Assign doufloat as responsible person
CchenyifengCollaborators
16 days ago Removed responsible person chen_0715
Bbaijinxiang_hwCollaborators
14 days ago Associated pull request
Bbaijinxiang_hwCollaborators
14 days ago Associated pull request
CANN-robotCANN-robotCollaborators
8 days ago Close issue