Closed
[Bug-Report|缺陷反馈]: aclnnMoeGatingTopKSoftmax 未校验输出末维与 k 的一致性,非法输出 shape 返回成功并留下未初始化内存 #6552
liuhanwen8Add on  17 days agoClosed at  10 days ago
liuhanwen8
liuhanwen8
17 days ago Create

Describe the current behavior / 问题描述 (Mandatory / 必填)

aclnnMoeGatingTopKSoftmax(V1)缺少「输出末维 == k」的一致性校验,且 k=0 时直接返回成功;同族 aclnnMoeGatingTopKSoftmaxV2 对完全相同的参数均返回 561002。

真机实测(Atlas A2,aclnn 两段式直调,输出 buffer 预填 NaN 金丝雀):

用例 V1 实测 V2 实测 资料要求
k=2,yOut/expertIdxOut/rowIdxOut = [4,3](末维 3 ≠ k) rc1=0, rc2=0 561002 561002("输入属性和输入tensor之间的shape信息不匹配")
k=0 rc1=0, rc2=0 561002 561002("k 的值小于或等于0")

k=2 / out=[4,3] 时算子只写了前 2 列,第 3 列原样保留调用方内存:

expertIdxOut = 4 7 7 2 5 4 2 7 | 2143289344 2143289344 2143289344 2143289344   (0x7FC00000)
rowIdxOut    = 0 4 1 5 2 6 3 7 | 2143289344 2143289344 2143289344 2143289344
yOut(bits)   = ...             | 0x7E00 0x7E00 0x7E00 0x7E00                    (fp16 NaN)

调用方拿到的是未初始化内存,两段接口都不报错 —— 属静默脏数据。

源码侧对照(master):moe/moe_gating_top_k_softmax_v2/op_host/moe_gating_top_k_softmax_v2_tiling_base.cpp 的 CheckOutShape() 有

OP_CHECK_IF((outShape.GetDim(lastDimNum) != k), ...);   // 第 78 行

而 V1 的 moe/moe_gating_top_k_softmax/op_host/moe_gating_top_k_softmax_tiling_base.cpp 中同名 CheckOutShape() 只校验了 rank、dim0、dim1,没有末维与 k 的判据。

Environment Information / 环境信息 (Mandatory / 必填)

  • 昇腾硬件型号:Atlas A2 训练系列产品(910B2)
  • CANN 版本:9.2.0(aarch64-linux,npu-smi 26.1.1)
  • 操作系统:Linux aarch64;g++ 支持 C++17

Steps to reproduce the issue / 重现步骤 (Mandatory / 必填)

source $ASCEND_HOME_PATH/set_env.sh
g++ -std=c++17 -O2 repro_moe_gating_topk.cpp -o repro \
    -I$ASCEND_HOME_PATH/include -L$ASCEND_HOME_PATH/lib64 -lascendcl -lnnopbase -lopapi
ASCEND_RT_VISIBLE_DEVICES=0 ./repro

repro_moe_gating_topk.cpp(自包含,仅依赖 CANN aclnn 头文件):

/**
 * 编译:g++ -std=c++17 -O2 repro_moe_gating_topk.cpp -o repro \
 *           -I$ASCEND_HOME_PATH/include -L$ASCEND_HOME_PATH/lib64 -lascendcl -lnnopbase -lopapi
 * 运行:ASCEND_RT_VISIBLE_DEVICES=0 ./repro
 */
#include <cstdio>
#include <cstdint>
#include <vector>

#include "acl/acl.h"
#include "aclnnop/aclnn_moe_gating_top_k_softmax.h"
#include "aclnnop/aclnn_moe_gating_top_k_softmax_v2.h"

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

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

int main()
{
    aclInit(nullptr);
    aclrtSetDevice(0);
    aclrtStream stream;
    aclrtCreateStream(&stream);

    // x: fp16 [4,8](内容无关,任取小值即可)
    const std::vector<int64_t> xs = {4, 8};
    std::vector<uint16_t> xh(Numel(xs), 0x2E66);   // ~0.1
    void *px = nullptr;
    aclrtMalloc(&px, xh.size() * 2, ACL_MEM_MALLOC_HUGE_FIRST);
    aclrtMemcpy(px, xh.size() * 2, xh.data(), xh.size() * 2, ACL_MEMCPY_HOST_TO_DEVICE);
    aclTensor *xT = MakeTensor(xs, ACL_FLOAT16, px);

    // A) k=0(资料要求 561002)
    {
        std::vector<int64_t> zs = {4, 0};
        void *py = nullptr, *pe = nullptr, *pr = nullptr;
        aclrtMalloc(&py, 2, ACL_MEM_MALLOC_HUGE_FIRST);
        aclrtMalloc(&pe, 2, ACL_MEM_MALLOC_HUGE_FIRST);
        aclrtMalloc(&pr, 2, ACL_MEM_MALLOC_HUGE_FIRST);
        aclTensor *y = MakeTensor(zs, ACL_FLOAT16, py);
        aclTensor *e = MakeTensor(zs, ACL_INT32, pe);
        aclTensor *r = MakeTensor(zs, ACL_INT32, pr);
        uint64_t ws = 0;
        aclOpExecutor *ex = nullptr;
        int rc1 = aclnnMoeGatingTopKSoftmaxGetWorkspaceSize(xT, nullptr, 0, y, e, r, &ws, &ex);
        int rc2 = (rc1 == 0) ? (int)aclnnMoeGatingTopKSoftmax(nullptr, ws, ex, stream) : -1;
        printf("[A] V1 k=0          rc1=%d rc2=%d  (expect 561002)\n", rc1, rc2);
        aclDestroyTensor(y); aclDestroyTensor(e); aclDestroyTensor(r);
        aclrtFree(py); aclrtFree(pe); aclrtFree(pr);
    }

    // B) k=2 而输出末维=3,输出预填 NaN 金丝雀
    {
        std::vector<int64_t> os = {4, 3};
        size_t n = (size_t)Numel(os);
        std::vector<uint16_t> yh(n, 0x7E00);                // fp16 NaN
        std::vector<int32_t> ih(n, (int32_t)0x7FC00000);
        void *py = nullptr, *pe = nullptr, *pr = nullptr;
        aclrtMalloc(&py, n * 2, ACL_MEM_MALLOC_HUGE_FIRST);
        aclrtMalloc(&pe, n * 4, ACL_MEM_MALLOC_HUGE_FIRST);
        aclrtMalloc(&pr, n * 4, ACL_MEM_MALLOC_HUGE_FIRST);
        aclrtMemcpy(py, n * 2, yh.data(), n * 2, ACL_MEMCPY_HOST_TO_DEVICE);
        aclrtMemcpy(pe, n * 4, ih.data(), n * 4, ACL_MEMCPY_HOST_TO_DEVICE);
        aclrtMemcpy(pr, n * 4, ih.data(), n * 4, ACL_MEMCPY_HOST_TO_DEVICE);
        aclTensor *y = MakeTensor(os, ACL_FLOAT16, py);
        aclTensor *e = MakeTensor(os, ACL_INT32, pe);
        aclTensor *r = MakeTensor(os, ACL_INT32, pr);
        uint64_t ws = 0;
        aclOpExecutor *ex = nullptr;
        int rc1 = aclnnMoeGatingTopKSoftmaxGetWorkspaceSize(xT, nullptr, 2, y, e, r, &ws, &ex);
        int rc2 = (rc1 == 0) ? (int)aclnnMoeGatingTopKSoftmax(nullptr, ws, ex, stream) : -1;
        if (rc2 == 0) aclrtSynchronizeStream(stream);
        std::vector<int32_t> eo(n, 0), ro(n, 0);
        std::vector<uint16_t> yo(n, 0);
        aclrtMemcpy(eo.data(), n * 4, pe, n * 4, ACL_MEMCPY_DEVICE_TO_HOST);
        aclrtMemcpy(ro.data(), n * 4, pr, n * 4, ACL_MEMCPY_DEVICE_TO_HOST);
        aclrtMemcpy(yo.data(), n * 2, py, n * 2, ACL_MEMCPY_DEVICE_TO_HOST);
        printf("[B] V1 k=2 out[4,3] rc1=%d rc2=%d  (expect 561002)\n", rc1, rc2);
        printf("    expertIdxOut = ");
        for (auto v : eo) printf("%d ", v);
        printf("\n    rowIdxOut    = ");
        for (auto v : ro) printf("%d ", v);
        printf("\n    yOut bits    = ");
        for (auto v : yo) printf("0x%04X ", v);
        printf("\n");
        aclDestroyTensor(y); aclDestroyTensor(e); aclDestroyTensor(r);
        aclrtFree(py); aclrtFree(pe); aclrtFree(pr);
    }

    // C) V2 同 B 参数(对照)
    {
        std::vector<int64_t> os = {4, 3};
        size_t n = (size_t)Numel(os);
        void *py = nullptr, *pe = nullptr;
        aclrtMalloc(&py, n * 2, ACL_MEM_MALLOC_HUGE_FIRST);
        aclrtMalloc(&pe, n * 4, ACL_MEM_MALLOC_HUGE_FIRST);
        aclTensor *y = MakeTensor(os, ACL_FLOAT16, py);
        aclTensor *e = MakeTensor(os, ACL_INT32, pe);
        uint64_t ws = 0;
        aclOpExecutor *ex = nullptr;
        int rc1 = aclnnMoeGatingTopKSoftmaxV2GetWorkspaceSize(xT, nullptr, 2, 0, false, y, e,
                                                              nullptr, &ws, &ex);
        printf("[C] V2 k=2 out[4,3] rc1=%d  (对照:V2 拒绝)\n", rc1);
        aclDestroyTensor(y); aclDestroyTensor(e);
        aclrtFree(py); aclrtFree(pe);
    }

    aclDestroyTensor(xT);
    aclrtFree(px);
    aclrtDestroyStream(stream);
    aclrtResetDevice(0);
    aclFinalize();
    return 0;
}

Describe the expected behavior / 预期结果 (Mandatory / 必填)

  1. 输出末维与 k 不一致时,第一段接口应返回 561002(资料错误码表:「输入属性和输入tensor之间的shape信息不匹配」),与 V2 一致;
  2. k=0 时应返回 561002(资料:「k 的值小于或等于0」);
  3. 不能出现「返回成功但输出缓冲区部分未写入」的情况。
[A] V1 k=0            rc1=0 rc2=0  (expect 561002)
[B] V1 k=2 out[4,3]   rc1=0 rc2=0  (expect 561002)
    expertIdxOut = 4 7 7 2 5 4 2 7 2143289344 2143289344 2143289344 2143289344
    rowIdxOut    = 0 4 1 5 2 6 3 7 2143289344 2143289344 2143289344 2143289344
    yOut bits    = 0x3826 0x2D38 0x3800 0x2DD8 0x30E1 0x30A4 0x37D9 0x2D74 0x7E00 0x7E00 0x7E00 0x7E00
[C] V2 k=2 out[4,3]   rc1=561002  (对照:V2 拒绝)

(2143289344 = 0x7FC00000,0x7E00 = fp16 NaN,均为调用方预填的金丝雀,算子未写入。)

Special notes for this issue/备注 (Optional / 选填)

  • 与 #6425、#6284 的关系:那两条是文档侧修正(把 k 下界统一为 k > 0、修正 rowIdx 公式),其结论是"算子会拒绝 k=0"。本次实测显示 aclnn 路径下 V1 并未拒绝 k=0(rc=0),与文档修正后对外承诺的行为不一致;本 issue 报的是实现侧校验缺失,不重复文档问题。
  • k=0 时三个输出均为 0 元素,怀疑是「输出为空 → 提前返回成功」的路径排在 tiling 的 k <= 0 检查之前;CheckOutShape 的末维判据则是 V2 有、V1 无。
  • 建议修复:在 V1 的 CheckOutShape() 中补 outShape.GetDim(lastDimNum) != k 判据(可直接复用 V2 的写法与错误码),并让空输出早退之前先做 k 的合法性校验。
likedislike
liuhanwen8liuhanwen8
17 days ago Added label: bug-report
周云飞
周云飞Collaborators
17 days ago Comments:

感谢提供完整的复现代码和金丝雀对照结果。这条反馈关注的是 V1 的实际参数校验行为,与您提到的文档修正不是同一层面的问题。

问题分析

您提供的 Atlas A2、CANN 9.2.0 日志显示,k=2、三个输出均为 [4,3] 时,V1 两段接口都返回成功,但每个输出的 12 个元素中仅前 8 个被改写,末尾 4 个仍保留预填值。这里更准确的描述是“输出缓冲区部分未写入”,而不是按 [4,3] 布局逐行只写前两列。如果下游按声明的形状读取全部元素,就会把这些残留值当作有效结果;预先清零只能掩盖现象,不能解决参数校验问题。

k=0 是另一条需要单独确认的路径。您的日志显示,零元素输出下 V1 仍返回成功;“空输出提前返回绕过了 k 校验”是合理的排查方向,但目前还不能确定具体执行顺序。V2 对输出末维不匹配返回 561002,已有您提供的对照日志支持;所附程序未包含 V2 的 k=0 对照调用,该项目前以您的实测描述为依据。

处理方向

一个可行的修正方向是在 V1 的输出形状检查中,分别校验 yOut、expertIdxOut、rowIdxOut 的末维是否等于 k,同时在可能的空输出成功返回之前检查 k 的合法性。这样可以拒绝非法参数,而不是通过填充多余输出位置掩盖形状不匹配。

按您引用的资料,这两类非法参数都应由第一段接口返回 561002。具体实现还需确认内部校验失败如何传递为公开接口错误码,并保留合法空张量场景原有的行为,不能简单禁止所有空输出。

在问题进一步确认前,调用侧可先检查 k > 0,并确保三个输出的末维均等于 k,不要使用上述非法参数下返回成功的输出。我们会继续确认 V1 的校验路径及错误码传递情况。

likedislike
游震Collaborators
17 days ago Comments:
likedislike
CANN-robotCANN-robotCollaborators
17 days ago Assign kdy18482276080 as responsible person
Kkdy18482276080Collaborators
17 days ago Assign weixin_44156099 as responsible person
Kkdy18482276080Collaborators
17 days ago Removed responsible person kdy18482276080
Kkdy18482276080Collaborators
17 days ago Assign kdy18482276080 as responsible person
Kkdy18482276080Collaborators
17 days ago Removed responsible person kdy18482276080
何宇航何宇航Collaborators
11 days ago Associated pull request
何宇航
何宇航Collaborators
11 days ago Comments:
CANN-robotCANN-robotCollaborators
10 days ago Close issue
CANN-robotCANN-robotCollaborators
10 days ago Added label: resolved