感谢提供详细的复现脚本、有限差分结果和并发对照实验。针对 Ascend 910B3、CANN 9.2.0 下的三类异常及头文件约束缺口,目前可以区分如下。
问题分析
-
B=0 时非空输出未写入,有明确的代码线索。 接口的空输入分支在发现 gradHin、x 等张量为空后,直接跳过计算,并将 workspaceSize 设为 0;该分支没有为非空参数梯度安排清零操作。正常计算路径中的输出拷贝也不会在这里执行。这与您报告的 gradPhi、gradAlpha、gradBias 分别保留 49152、12、96 字节预填值的现象一致。这里需要区分:gradX 本身为空,但三个参数梯度并不为空,不能仅因输入为空就省略它们的输出处理。相关实现见空输入判断及正常输出拷贝路径。
-
大形状梯度偏差需要继续定位具体计算环节。 您提供的 eps 扫描、前向重复性和 B/S 划分对照,是定位反向计算的重要依据。不过,仅凭这些摘录,还不能确定偏差来自哪一步归约、精度设置或中间数据处理,也不能直接认定它与并发异常具有相同根因。
-
混合形状并发异常需要与内核告警分别核对。 不同 case 混合并发失败、同 case 并发及 aclnnAdd 对照通过,为缩小排查范围提供了有效线索。但同 block 内的 WAR 告警本身不能证明存在跨调用共享数据污染;UB 未初始化读取是否影响最终输出,也需要结合实际读写范围确认。另外,B=0 输出未写入会使摘要依赖缓冲区原有内容,应与非空 case 的并发结果分开判断。
-
头文件确实缺少所述约束说明。 当前接口注释列出了形状和类型,但没有声明 N=4、迭代次数为 20、C 为 128 的倍数,也没有说明空输入时的输出语义。这三项限制的实际适用范围及返回 161002 的具体检查位置,仍需结合校验和 tiling 实现确认。
处理方向
对于 B=0,一个可行的调整是在空输入路径中,为非空的 gradPhi、gradAlpha、gradBias 安排写零操作,同时保留 gradX 为空的行为,并确保写零工作纳入两段式接口的执行流程。
对于精度和并发问题,建议分别沿大形状计算分支、归约有效范围与初始化范围,以及不同调用之间的状态隔离继续定位,暂不预设统一根因。
请补充您提到的有限差分完整脚本、混合形状并发 harness,以及对应的 mssanitizer 原始日志。前者用于核对参考梯度构造和中间量生成方式;后两者用于确认线程中的 context 设置、提交与同步顺序,并将告警对应到具体访问范围。B=0 的最小复现和环境信息已提供,无需重复补充。


已提交缺陷2(B=0 输出未写)的修复 PR:https://gitcode.com/cann/ops-transformer/merge_requests/14770
修复内容:backward 空 tensor 早退分支(任一输入为空,含 x 为空的 B=0 场景)现在对非空的 gradX/gradPhi/gradAlpha/gradBias 通过 l0op::Fill + ViewCopy 写零后再返回(空输入梯度数学上为全 0),workspaceSize 相应更新;与维护者分析的方向一致。
实机验证(910B3 A/B,金丝雀 0xAB 预填 49152B):修复前 gradPhi 全残留,修复后 全 0;有效路径(bs=2 正常 backward)gradPhi checksum 与修复前逐位一致,行为不变。
缺陷1(大 shape 梯度漂移)与缺陷3(并发输出污染)需要 FD 完整脚本、并发 harness 与 mssanitizer 原始日志复现定位,请提供后继续跟进;本 PR 暂不涉及。


Describe the current behavior / 问题描述 (Mandatory / 必填)
A2(Ascend 910B3)+ CANN 9.2.0,通过 libopapi 两段式纯 C 调用(ctypes + numpy,无 torch 依赖)实测
aclnnMhcPreSinkhornBackward存在 3 个缺陷:缺陷 1:大 shape 反向梯度与前向有限差分(FD)存在 ~2e-2 系统偏差
缺陷 2:B=0 空批次返回成功但不写非空输出
gradPhi 49152/49152、gradAlpha 12/12、gradBias 96/96字节全残留缺陷 3:共享 context 8 线程并发调用,输出被跨调用污染
mhc_pre_grad_kernel.h:938WholeReduceSum 读dpreLocalList_[outPos]vs:826Duplicate 清零,跨调用间无 barrier);initcheck 28 处 UB 越界读(BlockReduceSum:467/:539读越已写区域进未初始化尾部——串行下残渣稳定不影响输出,并发下随核间调度变化,与 seq512 case 随机摘要机制互证)另附一项 P3:头文件约束缺口——
N=4/numIters=20/C%128==0三项约束未在头文件声明,违反时返回 161002。Environment / 环境信息 (Mandatory / 必填)
Steps to reproduce the issue / 重现步骤 (Mandatory / 必填)
缺陷 2 一键复现:保存以下脚本为
repro_b0.py,python3 repro_b0.py [device_id]:#!/usr/bin/env python3 # -*- coding: utf-8 -*- """aclnnMhcPreSinkhornBackward 空批次(B=0)输出未写缺陷最小复现(自包含)。 现象:B=0 时两段式调用返回 0(成功),但非空输出 gradPhi/gradAlpha/gradBias 完全未被写出(保留预填金丝雀 0xAB)。数学上 B=0 的空和应写出全 0。 环境:Ascend 910B3(A2)+ CANN 9.2.0;仅依赖 ctypes + numpy,无需 torch。 """ import ctypes as C import os import sys import numpy as np ACL_FLOAT, ACL_BF16 = 0, 27 ACL_FORMAT_ND = 2 DTYPESZ = {ACL_FLOAT: 4, ACL_BF16: 2} H2D, D2H = 1, 2 HUGE_FIRST = 0 def find_cann(): cands = [os.environ.get("ASCEND_HOME_PATH"), "/usr/local/Ascend/ascend-toolkit/latest", "/usr/local/Ascend/nna/latest"] for p in cands: if not p: continue for sub in ("aarch64-linux/lib64", "lib64"): if os.path.isfile(os.path.join(p, sub, "libopapi.so")): return os.path.join(p, sub) raise SystemExit("CANN runtime (libopapi.so) not found; set ASCEND_HOME_PATH") class MiniAcl: def __init__(self): libdir = find_cann() self.ascendcl = C.CDLL(os.path.join(libdir, "libascendcl.so"), mode=C.RTLD_GLOBAL) self.nnopbase = C.CDLL(os.path.join(libdir, "libnnopbase.so"), mode=C.RTLD_GLOBAL) self.opapi = C.CDLL(os.path.join(libdir, "libopapi.so"), mode=C.RTLD_GLOBAL) rt = self.ascendcl for fn, at in (("aclInit", [C.c_void_p]), ("aclrtSetDevice", [C.c_int32]), ("aclrtResetDevice", [C.c_int32]), ("aclrtCreateStream", [C.POINTER(C.c_void_p)]), ("aclrtDestroyStream", [C.c_void_p]), ("aclrtSynchronizeStream", [C.c_void_p]), ("aclrtFree", [C.c_void_p])): getattr(rt, fn).argtypes = at getattr(rt, fn).restype = C.c_int32 rt.aclrtMalloc.argtypes = [C.POINTER(C.c_void_p), C.c_size_t, C.c_int32] rt.aclrtMalloc.restype = C.c_int32 rt.aclrtMemcpy.argtypes = [C.c_void_p, C.c_size_t, C.c_void_p, C.c_size_t, C.c_int32] rt.aclrtMemcpy.restype = C.c_int32 nn = self.nnopbase nn.aclCreateTensor.argtypes = [ C.POINTER(C.c_int64), C.c_uint64, C.c_int32, C.POINTER(C.c_int64), C.c_int64, C.c_int32, C.POINTER(C.c_int64), C.c_uint64, C.c_void_p] nn.aclCreateTensor.restype = C.c_void_p nn.aclDestroyTensor.argtypes = [C.c_void_p] op = self.opapi self.bwd_gws = op.aclnnMhcPreSinkhornBackwardGetWorkspaceSize self.bwd_gws.argtypes = [C.c_void_p] * 12 + [C.c_double] + [C.c_void_p] * 4 + \ [C.POINTER(C.c_uint64), C.POINTER(C.c_void_p)] self.bwd_gws.restype = C.c_int32 self.bwd_run = op.aclnnMhcPreSinkhornBackward self.bwd_run.argtypes = [C.c_void_p, C.c_uint64, C.c_void_p, C.c_void_p] self.bwd_run.restype = C.c_int32 def check(self, rc, what): if rc != 0: raise RuntimeError("%s failed, rc=%d" % (what, rc)) return rc class T: """张量:0 元素用 NULL 指针;非空输出预填金丝雀后可检查残留。""" def __init__(self, acl, dtype, shape): self.acl, self.dtype, self.shape = acl, dtype, tuple(shape) self.nbytes = (int(np.prod(self.shape)) if self.shape else 1) * DTYPESZ[dtype] self.dev, self.handle = C.c_void_p(0), None if self.nbytes: p = C.c_void_p() acl.check(acl.ascendcl.aclrtMalloc(C.byref(p), self.nbytes, HUGE_FIRST), "aclrtMalloc") self.dev = p def upload(self, raw): if self.nbytes: self.acl.ascendcl.aclrtMemcpy(self.dev, self.nbytes, raw.ctypes.data, self.nbytes, H2D) def download(self): if not self.nbytes: return np.zeros(0, dtype=np.uint8) buf = np.zeros(self.nbytes, dtype=np.uint8) self.acl.ascendcl.aclrtMemcpy(buf.ctypes.data, self.nbytes, self.dev, self.nbytes, D2H) return buf def handle_(self): dims = (C.c_int64 * len(self.shape))(*self.shape) st = [] acc = 1 for d in reversed(self.shape): st.append(acc) acc *= d st = (C.c_int64 * len(st))(*reversed(st)) h = self.acl.nnopbase.aclCreateTensor( dims, len(self.shape), self.dtype, st, 0, ACL_FORMAT_ND, dims, len(self.shape), self.dev) if not h: raise RuntimeError("aclCreateTensor returned NULL") self.handle = C.c_void_p(h) return self.handle def main(): dev = int(sys.argv[1]) if len(sys.argv) > 1 else 0 B, S, N, Ch = 0, 8, 4, 128 # B=0 触发 nc, sk = 2 * N + N * N, 40 # nc=24, sk=2*numIters(20) SENT = 0xAB acl = MiniAcl() acl.check(acl.ascendcl.aclInit(None), "aclInit") acl.check(acl.ascendcl.aclrtSetDevice(dev), "SetDevice") stream = C.c_void_p() acl.check(acl.ascendcl.aclrtCreateStream(C.byref(stream)), "CreateStream") rng = np.random.default_rng(20261023) ins = [T(acl, ACL_BF16, (B, S, Ch)), # gradHin T(acl, ACL_FLOAT, (B, S, N)), # gradHPost T(acl, ACL_FLOAT, (B, S, N * N)), # gradHRes T(acl, ACL_BF16, (B, S, N, Ch)), # x T(acl, ACL_FLOAT, (nc, N * Ch)), # phi T(acl, ACL_FLOAT, (3,)), # alpha T(acl, ACL_FLOAT, (nc,)), # bias T(acl, ACL_FLOAT, (B, S, N)), # hPre T(acl, ACL_FLOAT, (B, S, nc)), # hcBeforeNorm T(acl, ACL_FLOAT, (B, S, 1)), # invRms T(acl, ACL_FLOAT, (sk, B, S, N)), # sumOut T(acl, ACL_FLOAT, (sk, B, S, N, N))] # normOut ins[4].upload(np.ascontiguousarray( rng.standard_normal((nc, N * Ch)) * 0.05).astype(np.float32)) ins[5].upload(np.zeros(3, np.float32)) ins[6].upload(np.zeros(nc, np.float32)) outs = [T(acl, ACL_BF16, (B, S, N, Ch)), # gradX(0 元素) T(acl, ACL_FLOAT, (nc, N * Ch)), # gradPhi 非空 T(acl, ACL_FLOAT, (3,)), # gradAlpha 非空 T(acl, ACL_FLOAT, (nc,))] # gradBias 非空 for t in outs[1:]: t.upload(np.full(t.nbytes, SENT, dtype=np.uint8)) print("[cfg] B=%d S=%d N=%d C=%d: gradPhi=%dB gradAlpha=%dB gradBias=%dB" % (B, S, N, Ch, outs[1].nbytes, outs[2].nbytes, outs[3].nbytes)) args = [t.handle_() for t in ins] + [C.c_double(1e-6)] + [t.handle_() for t in outs] ws_size, executor = C.c_uint64(0), C.c_void_p() acl.check(acl.bwd_gws(*(args + [C.byref(ws_size), C.byref(executor)])), "GetWorkspaceSize") ws = C.c_void_p(0) if ws_size.value: acl.check(acl.ascendcl.aclrtMalloc(C.byref(ws), ws_size.value, HUGE_FIRST), "workspace malloc") acl.check(acl.bwd_run(ws, ws_size.value, executor, stream), "Execute") acl.check(acl.ascendcl.aclrtSynchronizeStream(stream), "Sync") print("[bwd] two-stage call OK (rc=0), workspaceSize=%d" % ws_size.value) bug = False for name, t in zip(("gradPhi", "gradAlpha", "gradBias"), outs[1:]): raw = t.download() left = int((raw == SENT).sum()) frac = left / len(raw) if len(raw) else 0.0 v = "UNWRITTEN" if frac == 1.0 else ("partial" if frac > 0 else "written") print("[chk] %-9s %5d/%-5d bytes still canary 0x%02X -> %s" % (name, left, len(raw), SENT, v)) bug = bug or frac > 0 print("=" * 60) if bug: print("RESULT: BUG - rc=0 but non-empty outputs unwritten (should be all-zero)") sys.exit(1) print("RESULT: PASS - all non-empty outputs written") sys.exit(0) if __name__ == "__main__": main()910B3 + CANN 9.2.0 实测输出:
缺陷 1 复现方法:前向(aclnnMhcPreSinkhorn)生成 sumOut/normOut 等输入 → 调 backward → 对 x/phi/alpha/bias 逐元素 ±eps 前向差分求 FD 梯度,eps 从 0.2 扫至 0.005;FD 各条目均平滑收敛而 backward 输出不变 → 系统偏差在 backward 侧(完整脚本
test_aclnn_mhc_presinkhorn_backward.py+fd_sweep.py可提供,同样为纯 ctypes 自包含实现)。缺陷 3 复现方法:8 线程共享同一 aclrt context(每调用独立 malloc 输入/输出/workspace、独立 executor),循环随机调用 8 条不同 shape 的 case,输出位模式(sha256 over raw bytes)与单线程基准比对;base 形状 case 稳定 FAIL、seq512/空批次 case 随机 FAIL;同 case 单独并发与 aclnnAdd 对照均 PASS(完整 harness 可提供)。
Describe the expected behavior / 预期结果 (Mandatory / 必填)
Related log / screenshot / 日志 / 截图 (Mandatory / 必填)
见上方证据摘录。FD 全表、mssanitizer(initcheck/racecheck)原始日志、并发比对明细均可提供。
Special notes for this issue/备注 (Optional / 选填)
已检索本仓相关 issue:#3929(算子支持需求建议)、#4706(format 校验一致性)、#5609(前向 mhc_pre_sinkhorn 的 HF32 精度声明与状态还原,已修复)——均与本问题不重叠(本 issue 全部针对反向算子 mhc_pre_sinkhorn_backward)。