Open
[Bug-Report|缺陷反馈]: aclnnBlockSparseAttention V1 全 0 blockSparseMask 时两段返回 0 但 attentionOut 完全未写入(哨兵 7.0 全残留) #7218
liuhanwen8Add on  1 day ago
liuhanwen8
liuhanwen8
1 day ago Create

关联:#6751(同一根因:有效块数为零时提前跳过导致输出未写入;本条差异:接口为 aclnnBlockSparseAttention V1,A2/CANN 9.3.0,触发条件为全 0 blockSparseMask)

现象

aclnnBlockSparseAttention(V1)在 blockSparseMask 全 0(所有块均不参与计算)时,两段接口均返回 0,但 attentionOut 完全没有被写入,保留调用方缓冲区的原值。

用例 blockSparseMask GetWorkspaceSize 第二段 out 内容
A(对照) 全 1 0 0 正常写入,16384/16384 不再是哨兵值
B(缺陷) 全 0 0 0 16384/16384 仍为哨兵 7.0

输出缓冲区在调用前统一预填 FP16 7.0(0x4700)作为哨兵,执行后按位比对。A 用例证明该调用参数合法、算子可正常写出;B 用例只剩 mask 不同,输出却完全未变。

期望

全 0 mask 时没有任何块参与计算,输出仍必须写入定义明确的结果(按同族设计应写 0;如采用部分结果路径,也应写中性值)。不能依赖输出缓冲区恰好为 0,更不能返回成功却保留调用方原值。

环境

  • 硬件:Ascend 910B2(A2)
  • CANN:9.3.0(weekly 20261104)
  • 仓库:cann/ops-transformer master(2026-11-06,sha 0e4967202d4679a15046f6f205f476f577136870)
  • 复现日期:2026-11-06

原因

blockSparseMask 全 0 时,kernel 侧没有任何可参与计算的 KV 块;当前路径直接跳过该行/块的计算,未调用等价于空行写出的逻辑,导致 attentionOut 未被写。与 #6751 在 GenericBlockSparseAttention 上定位的「有效块数为零提前 continue」属同一根因,但本接口是独立的 aclnn 接口与 kernel 路径。

复现步骤

  1. 保存下方代码为 repro_bsa_empty_mask.cpp
  2. source $ASCEND_HOME_PATH/set_env.sh
  3. g++ -O2 -o repro repro_bsa_empty_mask.cpp -I$ASCEND_HOME_PATH/include -L$ASCEND_HOME_PATH/lib64 -lascendcl -lopapi_transformer -lnnopbase
  4. ./repro 1(对照)与 ./repro 0(缺陷),按实际卡号修改 aclrtSetDevice

复现脚本

实测输出:

mask=1 gws=0 exec=0 nan=0/16384 same_as_fill=0/16384 first=15360
mask=0 gws=0 exec=0 nan=0/16384 same_as_fill=16384/16384 first=18176
#include <acl/acl.h>
#include <aclnnop/aclnn_block_sparse_attention.h>
#include <cstdio>
#include <cstdint>
#include <cstdlib>
#include <vector>

static aclTensor* mk(int64_t* d, int64_t n, aclDataType dt, void* p) {
    return aclCreateTensor(d, n, dt, nullptr, 0, ACL_FORMAT_ND, d, n, p);
}

int main(int argc, char** argv) {
    int maskVal = argc > 1 ? atoi(argv[1]) : 1;
    const int T = 128, H = 1, D = 128;
    size_t elems = (size_t)T * H * D, bytes = elems * 2;
    aclInit(nullptr); aclrtSetDevice(0);
    aclrtStream st = nullptr; aclrtCreateStream(&st);
    void *dq=nullptr,*dk=nullptr,*dv=nullptr,*dout=nullptr,*dm=nullptr;
    aclrtMalloc(&dq,bytes,ACL_MEM_MALLOC_HUGE_FIRST);
    aclrtMalloc(&dk,bytes,ACL_MEM_MALLOC_HUGE_FIRST);
    aclrtMalloc(&dv,bytes,ACL_MEM_MALLOC_HUGE_FIRST);
    aclrtMalloc(&dout,bytes,ACL_MEM_MALLOC_HUGE_FIRST);
    aclrtMalloc(&dm,1,ACL_MEM_MALLOC_HUGE_FIRST);
    uint16_t fill = 0x4700;                       // 7.0 哨兵
    std::vector<uint16_t> ones(elems, 0x3C00), sent(elems, fill);
    aclrtMemcpy(dq,bytes,ones.data(),bytes,ACL_MEMCPY_HOST_TO_DEVICE);
    aclrtMemcpy(dk,bytes,ones.data(),bytes,ACL_MEMCPY_HOST_TO_DEVICE);
    aclrtMemcpy(dv,bytes,ones.data(),bytes,ACL_MEMCPY_HOST_TO_DEVICE);
    aclrtMemcpy(dout,bytes,sent.data(),bytes,ACL_MEMCPY_HOST_TO_DEVICE);
    int8_t mv = (int8_t)maskVal;
    aclrtMemcpy(dm,1,&mv,1,ACL_MEMCPY_HOST_TO_DEVICE);
    int64_t td[3] = {T,H,D}, md[4] = {1,H,1,1};
    aclTensor* q=mk(td,3,ACL_FLOAT16,dq);
    aclTensor* k=mk(td,3,ACL_FLOAT16,dk);
    aclTensor* v=mk(td,3,ACL_FLOAT16,dv);
    aclTensor* o=mk(td,3,ACL_FLOAT16,dout);
    aclTensor* m=mk(md,4,ACL_INT8,dm);
    int64_t bs[2] = {128,128}, sq[1] = {T};
    aclIntArray* bshape = aclCreateIntArray(bs,2);
    aclIntArray* asl = aclCreateIntArray(sq,1);
    aclIntArray* aslkv = aclCreateIntArray(sq,1);
    char lq[16] = "TND", lkv[16] = "TND";
    uint64_t ws = 0; aclOpExecutor* ex = nullptr;
    aclnnStatus r = aclnnBlockSparseAttentionGetWorkspaceSize(
        q,k,v,m,nullptr,bshape,asl,aslkv,nullptr,lq,lkv,1,0,0.088388347648,0,0,
        2147483647,2147483647,0,o,nullptr,&ws,&ex);
    printf("mask=%d gws=%d", maskVal, (int)r);
    if (r == 0) {
        void* w = nullptr; if (ws) aclrtMalloc(&w,ws,ACL_MEM_MALLOC_HUGE_FIRST);
        aclnnStatus r2 = aclnnBlockSparseAttention(w,ws,ex,st);
        aclrtSynchronizeStream(st);
        std::vector<uint16_t> out(elems,0);
        aclrtMemcpy(out.data(),bytes,dout,bytes,ACL_MEMCPY_DEVICE_TO_HOST);
        size_t same = 0; for (auto x : out) if (x == fill) same++;
        printf(" exec=%d same_as_fill=%zu/%zu first=%u", (int)r2, same, elems, (unsigned)out[0]);
    }
    printf("\n"); fflush(stdout);
    aclFinalize();
    return 0;
}

影响

全 0 稀疏 mask(如因果过滤后无有效块)时,调用方拿到返回码 0,但 attentionOut 是调用方缓冲区里的旧值或未初始化内容;该结果会继续流入后续网络层,造成随机的错误输出,且不报错。

建议

在「无有效块」分支为对应 Query 行写入定义明确的空行结果(0 或中性值),与 #6751 的 WriteEmptyOutput 设计保持一致;补充「blockSparseMask 全 0 + 输出缓冲区预填哨兵」的 UT。

likedislike
liuhanwen8liuhanwen8
1 day ago Added label: bug-report
周云飞
周云飞Collaborators
1 day ago Comments:

问题说明

您提供的对照结果表明:在 A2、CANN 9.3.0 的 V1 调用中,全 0 blockSparseMask 下两段接口均返回成功,但 16384 个输出元素仍保留哨兵值,存在输出未更新的异常。当前证据尚不能确认具体的 kernel 跳过分支,也不能确认与 #6751 根因相同。

处理建议

可考虑在 V1 的无有效 KV 块分支显式写出空行结果,避免依赖输出缓冲区初值;具体是否写 0,需结合该接口的空行语义确认。回归建议保留全 0 mask 的哨兵检查,并覆盖部分 Query 行无有效块的情况,确认空行被写出且正常行结果不受影响。

likedislike