Closed
[Bug-Report|缺陷反馈]: aclnnFmodScalar 误用 Tensor-Tensor 推导表,int8 与 uint8 组合被拒(161002),资料 Tensor-Scalar 推导表应得 int8/uint8 #4177
liuhanwen8Add on  13 days agoClosed at  7 days ago
liuhanwen8
liuhanwen8
13 days ago Create

一、问题描述 (必填)

aclnnFmodScalar / aclnnInplaceFmodScalar 的 other 形参类型是 const aclScalar*(Tensor-Scalar 接口),但其数据类型推导调用的是 op::PromoteType(Tensor-Tensor 推导表),而不是资料规定的 Tensor-Scalar 推导表。

后果:self=int8 且 other=uint8(以及反向 self=uint8 + other=int8)这 2 种资料允许的合法组合被拒绝,返回 161002。

资料依据(docs/zh/context/deduction_relationship.md)

  • 表1 TensorTensor 互推导关系(适用于两端都是 aclTensor 的接口,如 aclnnFmodTensor):s8 × u8 → s16
  • 表2 Tensor-Scalar 互推导关系(适用于一端是 aclTensor、一端是 aclScalar 的接口,如本仓 aclnnAdds、aclnnMuls、本接口):s8 × u8 → s8,u8 × s8 → u8

推导表说明原文:

当一个API(如aclnnAdds、aclnnMuls等)输入的Tensor数据类型和输入的Scalar数据类型不一致时,API内部会推导出一个数据类型…

本接口 other 是 aclScalar,属于表2 适用场景。

实测结果(8×8 全部 64 组 dtype 组合,逐组直调)

self other(aclScalar) 表2 应得 表1 应得 实测 r1 判定
int8 uint8 s8(在支持列表 → 应通过) s16(不在支持列表) 161002 不符表2,符合表1
uint8 int8 u8(在支持列表 → 应通过) s16(不在支持列表) 161002 不符表2,符合表1
其余 62 组 — — — 与两表均一致(推导结果恰好相同或都在/都不在支持列表) 一致

统计:64 组中与表1 不一致 0 组、与表2 不一致 2 组。即实现完全按表1 推导。

支持列表(源码 aclnn_fmod_scalar.cpp:36-38,910B)为
{FLOAT, INT32, INT8, FLOAT16, UINT8, DOUBLE, INT64, BF16} —— 不含 INT16。
表1 推出的 s16 不在列表内,于是被 CheckPromoteType 拒绝为 161002;表2 推出的 s8/u8 在列表内,本应正常计算。

代码定位

math/mod/op_api/aclnn_fmod_scalar.cpp:130-139

static DataType PromoteTypeScalar(const op::DataType selfDtype, const op::DataType otherDtype)
{
    if (IsRegBase()) {
        auto otherDefaultDtype = GetScalarDefaultDtype(otherDtype);
        auto promoteType = CombineCategoriesWithComplex(selfDtype, otherDefaultDtype);
        return promoteType;
    }

    return op::PromoteType(selfDtype, otherDtype);   // ← 非 RegBase 分支直接套用 Tensor-Tensor 推导
}

函数名叫 PromoteTypeScalar,但非 RegBase 分支的实现是 op::PromoteType(表1语义),名实不符。

IsRegBase() 仅在 DAV_3510 / DAV_9201 / DAV_9202 为真(common/inc/op_api/aclnn_check.h:196-207),Ascend 910B 走 else 分支,因此必现。

同仓同类实现对照(同类接口的正确写法)

math/floor_mod/op_api/aclnn_remainder.cpp:133-148 —— 同为 Tensor-Scalar 接口,其 PromoteTypeScalar 明确区分了 scalar 语义:

// tensor + scalar混合场景下,推导出应该cast的dtype (并不是promoteType)
static inline DataType PromoteTypeScalar(const op::DataType selfDtype, const op::DataType otherDtype)
{
    auto npuArch = op::GetCurrentPlatformInfo().GetCurNpuArch();
    if (IsRegBase(npuArch)) {
        return PromoteTypeScalarV35(selfDtype, otherDtype);
    }

    if (IsFloatingType(selfDtype)) {
        return selfDtype;
    } else {
        if (IsFloatingType(otherDtype) || selfDtype == op::DataType::DT_BOOL) {
            return op::PromoteType(selfDtype, otherDtype);
        }
        return selfDtype;              // ← 整型 tensor + 整型 scalar → 保持 tensor dtype(即表2 语义)
    }
}

同文件还配套了独立的 CheckPromoteTypeTensorScalar(:204-230),注释写明是「tensor + scalar 混合场景」。

而 aclnn_fmod_scalar.cpp 只有一个 CheckPromoteType,Tensor-Tensor 与 Tensor-Scalar 未做区分。

影响

  1. int8 / uint8 是量化场景的常用 dtype,aclnnFmodScalar(标量取模,如量化后做范围折叠)在 int8 张量配 uint8 标量(或反向)时直接报 161002,而这是资料允许的组合;
  2. 报错文案会显示「need promote dtype to INT16, but now not support promote dtype INT16」,把调用方引向「INT16 不支持」这个错误结论,而真实原因是选错了推导表;
  3. aclnnInplaceFmodScalar 共用同一 GetWorkspaceSizeCommon,同样受影响。

二、环境信息 (可选)

  • 昇腾硬件型号:Ascend 910B3(npu-smi 26.1.1 显示 910B3,8 die)
  • CANN 版本:9.2.0(/usr/local/Ascend/cann-9.2.0)
  • 操作系统:Linux aarch64;g++ 10.3.1
  • 其他:libopapi_math.so 提供 aclnnFmodScalar 符号;IsRegBase() 对 910B 为 false,必现;该 SoC 的 Mod kernel 走 CANN 内置 ops_legacy 实现,但本问题发生在 aclnn 接口层(host 侧)的参数校验阶段,在到达 kernel 之前就被拒绝,与 kernel 版本无关
  • 仓库基线:cann/ops-math master d8fc7a6

三、重现步骤 (可选)

自包含 C++ 复现(8×8 全组合扫描,一次跑完 64 组):

// build:
//   g++ -O2 -std=c++17 repro.cpp -o fmod_promote \
//     -I/usr/local/Ascend/cann-9.2.0/include \
//     -L/usr/local/Ascend/cann-9.2.0/aarch64-linux/lib64 \
//     -lascendcl -lnnopbase -lopapi_math -lopapi
// run:
//   source /usr/local/Ascend/ascend-toolkit/set_env.sh
//   export LD_LIBRARY_PATH=/usr/local/Ascend/cann-9.2.0/aarch64-linux/lib64:$LD_LIBRARY_PATH
//   ./fmod_promote

#include <cstdio>
#include <cstring>
#include <cstdint>
#include <vector>
#include <unistd.h>
#include "acl/acl.h"
#include "aclnnop/aclnn_fmod_scalar.h"

struct D { const char* n; aclDataType a; int sz; };
static D kD[] = {
    {"f32", ACL_FLOAT, 4}, {"f16", ACL_FLOAT16, 2}, {"f64", ACL_DOUBLE, 8}, {"bf16", ACL_BF16, 2},
    {"s8",  ACL_INT8, 1},  {"u8",  ACL_UINT8, 1},   {"s32", ACL_INT32, 4},  {"s64", ACL_INT64, 8},
};

int main()
{
    setvbuf(stdout, nullptr, _IONBF, 0);
    int32_t dev = 0;
    const char* e = getenv("FMOD_DEV");
    if (e != nullptr) dev = atoi(e);
    aclInit(nullptr);
    aclrtSetDevice(dev);

    for (int i = 0; i < 8; i++) {        // self = kD[i]
        for (int j = 0; j < 8; j++) {    // other = kD[j](aclScalar)
            int64_t n = 8;
            std::vector<int64_t> sh = {4, 2};
            std::vector<int64_t> sd = {2, 1};
            void* dS = nullptr;
            void* dO = nullptr;
            aclrtMalloc(&dS, (size_t)(n * kD[i].sz), ACL_MEM_MALLOC_HUGE_FIRST);
            aclrtMalloc(&dO, (size_t)(n * kD[i].sz), ACL_MEM_MALLOC_HUGE_FIRST);
            std::vector<uint8_t> hb((size_t)(n * kD[i].sz), 0);
            for (int64_t q = 0; q < n; q++) {
                double v = (double)(q * 5 + 2);
                uint8_t* p = hb.data() + q * kD[i].sz;
                if (strcmp(kD[i].n, "f32") == 0)       { float f = (float)v; memcpy(p, &f, 4); }
                else if (strcmp(kD[i].n, "f16") == 0)  { uint16_t h = aclFloatToFloat16((float)v); memcpy(p, &h, 2); }
                else if (strcmp(kD[i].n, "f64") == 0)  { memcpy(p, &v, 8); }
                else if (strcmp(kD[i].n, "bf16") == 0) { uint32_t u; float f = (float)v; memcpy(&u, &f, 4);
                                                         uint16_t b = (uint16_t)(u >> 16); memcpy(p, &b, 2); }
                else if (strcmp(kD[i].n, "s8") == 0)   { int8_t x = (int8_t)(v - 30); memcpy(p, &x, 1); }
                else if (strcmp(kD[i].n, "u8") == 0)   { uint8_t x = (uint8_t)(v + 40); memcpy(p, &x, 1); }
                else if (strcmp(kD[i].n, "s32") == 0)  { int32_t x = (int32_t)v; memcpy(p, &x, 4); }
                else if (strcmp(kD[i].n, "s64") == 0)  { int64_t x = (int64_t)v; memcpy(p, &x, 8); }
            }
            aclrtMemcpy(dS, (size_t)(n * kD[i].sz), hb.data(), (size_t)(n * kD[i].sz), ACL_MEMCPY_HOST_TO_DEVICE);

            uint8_t sb[8] = {0};
            double ov = 3.0;
            if (strcmp(kD[j].n, "f32") == 0)       { float f = (float)ov; memcpy(sb, &f, 4); }
            else if (strcmp(kD[j].n, "f16") == 0)  { uint16_t h = aclFloatToFloat16((float)ov); memcpy(sb, &h, 2); }
            else if (strcmp(kD[j].n, "f64") == 0)  { memcpy(sb, &ov, 8); }
            else if (strcmp(kD[j].n, "bf16") == 0) { uint32_t u; float f = (float)ov; memcpy(&u, &f, 4);
                                                     uint16_t b = (uint16_t)(u >> 16); memcpy(sb, &b, 2); }
            else if (strcmp(kD[j].n, "s8") == 0)   { int8_t x = 3; memcpy(sb, &x, 1); }
            else if (strcmp(kD[j].n, "u8") == 0)   { uint8_t x = 3; memcpy(sb, &x, 1); }
            else if (strcmp(kD[j].n, "s32") == 0)  { int32_t x = 3; memcpy(sb, &x, 4); }
            else if (strcmp(kD[j].n, "s64") == 0)  { int64_t x = 3; memcpy(sb, &x, 8); }

            aclTensor* self  = aclCreateTensor(sh.data(), 2, kD[i].a, sd.data(), 0, ACL_FORMAT_ND, sh.data(), 2, dS);
            aclTensor* out   = aclCreateTensor(sh.data(), 2, kD[i].a, sd.data(), 0, ACL_FORMAT_ND, sh.data(), 2, dO);
            aclScalar* other = aclCreateScalar(sb, kD[j].a);

            uint64_t ws = 0;
            aclOpExecutor* ex = nullptr;
            aclnnStatus r1 = aclnnFmodScalarGetWorkspaceSize(self, other, out, &ws, &ex);
            printf("self=%-4s other=%-4s r1=%d\n", kD[i].n, kD[j].n, (int)r1);

            aclDestroyTensor(self); aclDestroyTensor(out); aclDestroyScalar(other);
            aclrtFree(dS); aclrtFree(dO);
        }
    }
    fflush(stdout);
    _exit(0);
}

关键两行输出(Ascend 910B3 / CANN 9.2.0):

self=s8   other=u8   r1=161002     ← 表2 应得 s8(在支持列表),应返回 0
self=u8   other=s8   r1=161002     ← 表2 应得 u8(在支持列表),应返回 0
self=s8   other=s8   r1=0          (对照:同 dtype 正常)
self=s8   other=s32  r1=0          (对照:表1/表2 结果相同,正常)

完整 64 组与两表的比对统计:

与表1(TensorTensor)不一致:0 组
与表2(TensorScalar)不一致:2 组(s8×u8、u8×s8)

四、预期结果 (可选)

self=int8, other(aclScalar)=uint8 应按表2 推导为 int8 并正常执行(返回 0);self=uint8, other=int8 应推导为 uint8 并返回 0。

建议修复方向(与同仓 aclnn_remainder.cpp 对齐,math/mod/op_api/aclnn_fmod_scalar.cpp:130-139):

static DataType PromoteTypeScalar(const op::DataType selfDtype, const op::DataType otherDtype)
{
    if (IsRegBase()) {
        auto otherDefaultDtype = GetScalarDefaultDtype(otherDtype);
        return CombineCategoriesWithComplex(selfDtype, otherDefaultDtype);
    }

    // Tensor-Scalar 场景:照 TensorScalar 推导表(表2)语义
    if (IsFloatingType(selfDtype)) {
        return selfDtype;
    }
    if (IsFloatingType(otherDtype) || selfDtype == op::DataType::DT_BOOL) {
        return op::PromoteType(selfDtype, otherDtype);
    }
    return selfDtype;
}

同时建议把 CheckPromoteType 拆成 CheckPromoteTypeTensorScalar / 复用现有实现,避免 Tensor-Tensor 与 Tensor-Scalar 两条语义混用。

💡 备注(选填)

为什么这不是「能力不支持」而是「选错推导表」

  1. int8 与 uint8 各自都在 支持列表内(aclnn_fmod_scalar.cpp:36-38);被拒的不是输入 dtype,而是推导出的 INT16。
  2. 同仓 Tensor-Scalar 接口 aclnnRemainder(aclnn_remainder.cpp:133-148)对同样输入返回 selfDtype,即 s8/u8,与表2 一致 —— 说明本仓已有正确写法可参照。
  3. aclnnFmodTensor(真正的 Tensor-Tensor 接口,aclnn_fmod_tensor.cpp:77,144)用 op::PromoteType 是正确的;本单指出的是 FmodScalar 照抄了 Tensor 版推导。

与已有 issue 的关系(查重说明)

已翻查 cann/ops-math 全部 1200 条 issue:

  • 不重复 #4149(aclnnFmodTensor/aclnnFmodScalar 头文件公式用 floor 与资料 trunc 不符)—— 那条是头文件公式口径,本单是 dtype 推导表选择;
  • 不重复 #4006(aclnnInplaceRemainderTensorScalar 大数值域取模塌成 0)—— 那条是数值精度,本单是入参校验阶段被拒。

本次挖掘已核对一致的项(同批排查,供参考排除范围):8 种 dtype 单 dtype 路径全部可用且数值正确;rank 0-8 边界正确;out shape 不一致正确返回 161002;self 不在支持面(int16/bool/complex64)正确返回 161002;空 tensor 返回 0;NaN/±inf/除零特殊值符合 IEEE-754;精度(336 组)+ 正常场景 fuzz(1000 组)+ 多线程并发(4 线程 × 100 次)全部 0 失败。

likedislike
陈思
陈思Collaborators
12 days ago Comments:

您好,感谢反馈!

问题分析

已核对当前固定提交 8c1f633e3679 的 FmodScalar 实现 与 Tensor-Scalar 推导表。PromoteTypeScalar 在非 RegBase 分支调用 op::PromoteType,CheckPromoteType 又用算子支持列表检查推导结果。INT8 与 UINT8 按这条路径推导到 INT16,而该支持列表不含 INT16,因此会在执行 kernel 前返回参数无效。两种输入各自受支持,并不意味着当前推导后的类型也受支持。

资料表 2 的表头是 Tensor 类型、左列是 Scalar 类型;对应的两种整型组合保留 Tensor 的 INT8/UINT8 类型。这与您报告的 910B 路径存在差异。两个 Scalar 接口共用 GetWorkspaceSizeCommon,因此该选择同时影响原地和非原地入口;本轮确认的是源码机制,未重跑您提供的 64 组真机矩阵。

处理方向

一个可行的方向是在非 RegBase 的 Scalar 路径采用 Tensor-Scalar 推导规则,并让参数预检查与实际 Cast 使用同一推导结果,避免只改检查后仍按旧类型计算。该调整应保留浮点标量、输出转换及现有平台分支的约束,不能通过无条件增加 INT16 支持来替代推导语义核对;真正的 Tensor-Tensor 接口也应继续保留自己的规则。我们会继续确认这条 Scalar 路径及两入口的兼容边界。

likedislike
陈思陈思Collaborators
12 days ago Assign chen_0715 as responsible person
陈思陈思Collaborators
12 days ago Added label: pending-confirmation
CchenyifengCollaborators
12 days ago Assign m0_55003149 as responsible person
CchenyifengCollaborators
12 days ago Removed responsible person chen_0715
weikeweikeCollaborators
8 days ago Associated pull request
CANN-robotCANN-robotCollaborators
7 days ago Close issue
CANN-robotCANN-robotCollaborators
7 days ago Added label: resolved