关联:#6751(同一根因:有效块数为零时提前跳过导致输出未写入;本条差异:接口为 aclnnBlockSparseAttention V1,A2/CANN 9.3.0,触发条件为全 0 blockSparseMask)
aclnnBlockSparseAttention(V1)在 blockSparseMask 全 0(所有块均不参与计算)时,两段接口均返回 0,但 attentionOut 完全没有被写入,保留调用方缓冲区的原值。
aclnnBlockSparseAttention
blockSparseMask
attentionOut
输出缓冲区在调用前统一预填 FP16 7.0(0x4700)作为哨兵,执行后按位比对。A 用例证明该调用参数合法、算子可正常写出;B 用例只剩 mask 不同,输出却完全未变。
7.0
全 0 mask 时没有任何块参与计算,输出仍必须写入定义明确的结果(按同族设计应写 0;如采用部分结果路径,也应写中性值)。不能依赖输出缓冲区恰好为 0,更不能返回成功却保留调用方原值。
cann/ops-transformer
0e4967202d4679a15046f6f205f476f577136870
blockSparseMask 全 0 时,kernel 侧没有任何可参与计算的 KV 块;当前路径直接跳过该行/块的计算,未调用等价于空行写出的逻辑,导致 attentionOut 未被写。与 #6751 在 GenericBlockSparseAttention 上定位的「有效块数为零提前 continue」属同一根因,但本接口是独立的 aclnn 接口与 kernel 路径。
repro_bsa_empty_mask.cpp
source $ASCEND_HOME_PATH/set_env.sh
g++ -O2 -o repro repro_bsa_empty_mask.cpp -I$ASCEND_HOME_PATH/include -L$ASCEND_HOME_PATH/lib64 -lascendcl -lopapi_transformer -lnnopbase
./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。
WriteEmptyOutput
您提供的对照结果表明:在 A2、CANN 9.3.0 的 V1 调用中,全 0 blockSparseMask 下两段接口均返回成功,但 16384 个输出元素仍保留哨兵值,存在输出未更新的异常。当前证据尚不能确认具体的 kernel 跳过分支,也不能确认与 #6751 根因相同。
可考虑在 V1 的无有效 KV 块分支显式写出空行结果,避免依赖输出缓冲区初值;具体是否写 0,需结合该接口的空行语义确认。回归建议保留全 0 mask 的哨兵检查,并覆盖部分 Query 行无有效块的情况,确认空行被写出且正常行结果不受影响。
关联:#6751(同一根因:有效块数为零时提前跳过导致输出未写入;本条差异:接口为 aclnnBlockSparseAttention V1,A2/CANN 9.3.0,触发条件为全 0 blockSparseMask)
现象
aclnnBlockSparseAttention(V1)在blockSparseMask全 0(所有块均不参与计算)时,两段接口均返回 0,但attentionOut完全没有被写入,保留调用方缓冲区的原值。输出缓冲区在调用前统一预填 FP16
7.0(0x4700)作为哨兵,执行后按位比对。A 用例证明该调用参数合法、算子可正常写出;B 用例只剩 mask 不同,输出却完全未变。期望
全 0 mask 时没有任何块参与计算,输出仍必须写入定义明确的结果(按同族设计应写 0;如采用部分结果路径,也应写中性值)。不能依赖输出缓冲区恰好为 0,更不能返回成功却保留调用方原值。
环境
cann/ops-transformermaster(2026-11-06,sha0e4967202d4679a15046f6f205f476f577136870)原因
blockSparseMask全 0 时,kernel 侧没有任何可参与计算的 KV 块;当前路径直接跳过该行/块的计算,未调用等价于空行写出的逻辑,导致attentionOut未被写。与 #6751 在 GenericBlockSparseAttention 上定位的「有效块数为零提前 continue」属同一根因,但本接口是独立的 aclnn 接口与 kernel 路径。复现步骤
repro_bsa_empty_mask.cppsource $ASCEND_HOME_PATH/set_env.shg++ -O2 -o repro repro_bsa_empty_mask.cpp -I$ASCEND_HOME_PATH/include -L$ASCEND_HOME_PATH/lib64 -lascendcl -lopapi_transformer -lnnopbase./repro 1(对照)与./repro 0(缺陷),按实际卡号修改aclrtSetDevice复现脚本
实测输出:
#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。