Pull Request已成功合入, 合并人@CANN-robot
(感谢 luozhonglin 的贡献)变更摘要
该 PR 主要围绕 engram 反向梯度拉取流程的**超时诊断(DFX)**与确定性失败能力做增强:为 Kernel 侧四个卡死等待点引入 TimeoutSite 超时部位标记,使超时报错能直接定位等待位置;同时将自研 RUNTIME_ABORT 宏全面替换为 ascendc_assert,并移除通信原语的重试循环;Host 侧 tiling 新增 sort/unique 两个阶段的 UB 池峰值 fail-fast 校验,容量问题在算图阶段即确定性失败;另外调整了 sort 阶段 tile 到核的分发方式与收/发核划分逻辑。
主要改动
-
超时定位打点:
TimeoutCheck增加TimeoutSite参数,通过新增枚举(TIMEOUT_CREDIT_READ_WAIT、TIMEOUT_STATUS_FLAG_WAIT、TIMEOUT_SEND_CREDIT_WAIT、TIMEOUT_RECV_COUNTER_WAIT)标记CompleteCreditCounter、WaitAllStatusFlags、SendGradRemote、RecvGradFromPeers四个等待点,超时信息输出对应 tag,便于定位卡死位置。 -
错误处理统一为
ascendc_assert:删除engram_fetch_grad_utils.h中的RUNTIME_ABORT宏定义,将WriteNbiChecked、DrainChecked、PrefetchCreditCounter、rankSize 一致性校验、SendGradRemote、FlushAccum等处的RUNTIME_ABORT/重试逻辑全部改为ascendc_assert,同时移除WriteNbi、Drain、WriteWithNotifyNbi的COMM_RETRY_COUNT重试循环,通信失败改为一次性确定性失败。 -
UB 池 fail-fast 建模:
engram_fetch_grad_tiling.cpp新增CalcSortUbBytes(镜像 sort 各缓冲常量并随核数计算 sort 池峰值),在SetTilingData中新增 sort 阶段与 unique 阶段(GRAD_BUF_BYTES + uniqueEntryBytes + accumNeed + cast)两个池溢出检查,超限直接返回GRAPH_FAILED,并相应重算availableForCast。 -
entryBuf_按需分配:Kernel 侧新增UniqueEntryBytes计算 unique 阶段 entry 缓冲实际字节数,Process中entryBuf_由固定ENTRY_BUF_BYTES改为按uniqueEntryBytes_初始化,与 Host 侧建模保持一致。 -
核划分与 tile 分发调整:
Init中numRecvCores_改为totalBlocks_ - numSendCores_ - 1(预留一核);ProcessHist/ProcessScatter的 tile 分配由"按 batch 跨核均分"改为"按核连续分片"(coreStart = coreId * batchCount),使每个核处理连续的 tile 区间。


Thanks for your pull-request.
The full list of commands accepted by me can be found at here.
You can get sig-info at here.
You can self-configure the PR merge rules for this repository. For more details, please refer to Here.
For more, you also can visit HICANN.
PR Approval Progress
✅ Congratulations! All modules have met the lgtm and approve requirements.
Module Approval Details
| module | lgtm status | approve status |
|---|---|---|
| mc2 | ✅ wang-minbo, tgwsakiko_ (2/2) | ✅ wang-minbo (1/1) |
💡 Tip:
- Committer can comment
/approveor/lgtm- Commenting
/approveimplies both code review (lgtm) and intent to merge (approve)
CLA Signature Pass
luozhonglin222, thanks for your pull request. All authors of the commits have signed the CLA. 👍


此 PR 将 entryBuf_ 的分配大小从常量 Mc2Kernel::ENTRY_BUF_BYTES(64KB)改为动态 uniqueEntryBytes_,但 FlushAccum 中的 assert(第 471 行)仍以 Mc2Kernel::ENTRY_BUF_BYTES 为上限检查 castTailBytes。当 hiddenDim > 11264 时,uniqueEntryBytes_ = FLUSH_CAST_HEAD_BYTES + 2*align(hiddenDim*2, 32) > 65536,assert 会误触发(false positive),即使 entryBuf_ 实际分配空间足够。请将 assert 的上限改为 uniqueEntryBytes_(或 entryBuf_.GetSize())以匹配实际 buffer 大小。


changed this line on ce8c2ddc view diff detail
compile


| 🚀 CI 流水线已启动 |
|---|
| 📋 执行详情: 点击查看流水线 |


/lgtm


/lgtm
/approve


The MR can not be merged, because of CodeReview discussion not resolved
If you want to solve this problem, you can click here to do it in the FAQs.


描述
1.反向超时的DFX
2.UB溢出修复
3.scatter汇总和sort分核一致性修复
关联的Issue
测试
文档更新
类型标签
检视概览
code_summary.md(代码概要+变量溯源)、api_prestudy.md(API 约束预研)检视统计
核心结论
6 个 FAIL 高度同源,可归并为 2 个根因:
while(true)release 兜底)并将 8 处失败点改为ascendc_assert。经 CANN 9.0.0 头文件实证(kernel_operator_dump_tensor_intf.h:65-68),未定义ASCENDC_DUMP时ascendc_assert展开为空宏,且本算子构建未定义ASCENDC_DUMP。release 产物下:commContext(设备侧外部数据)rankSize 一致性校验完全失效 → 除零 UB / workspace displs 越界写;pendingHandles 固定数组(8 元素)越界守卫失效;通信失败/超时静默继续(且删除了原有 3 次重试)。recvIdx = aivId_ - numSendCores_在 aivId_=0 时发生 uint32 回绕,接收循环零迭代,单核形态多 rank 场景远端梯度静默丢失(功能回归)。正面确认:sort tile 归属 core-major 改写修复了 tileOffsets 跨核免屏障假设失效的竞态(多方交叉验证一致);UB 池收缩(entryBuf_ 64KB→uniqueEntryBytes_)Host/Kernel 两侧公式逐项核算等值、构造性安全;新增两道 Host fail-fast 校验(sortUb 峰值 / minUniqueNeed)release 生效,覆盖 Host 可建模部分。
发现问题(HIGH 置信度)
[cpp-secure 2.2] 无符号回绕 —— recvIdx 回绕致单核形态接收零迭代
numSendCores_旧值totalBlocks_-1U(totalBlocks_==1 时为 0)改为 1)后,RecvGradFromPeers 中无守卫的 uint32 减法aivId_ - numSendCores_在 totalBlocks_==1 && numRanks_>1 时发生 0-1=0xFFFFFFFF 回绕。totalBlocks_==1 时isReceiver_恒为 true(:525),回绕后的 recvIdx 使for (wIdx = recvIdx; wIdx < totalWorkUnits; ...)零迭代——本核静默跳过全部接收,对端发送核在 credit/tokenRead 计数器上自旋直至超时。旧代码该形态 numSendCores_=0、recvIdx=0 可完整覆盖(功能回归);Host 侧无 aivNum 与 rankSize 的关联校验,PR 又为 totalBlocks_<=1 专门设置了同核收发 fallback 分支,说明该形态是被明确支持的目标场景。numSendCores_ = sendersPerRank_ * numRanks_; if (numSendCores_ > halfBlocks) { numSendCores_ = halfBlocks; } numRecvCores_ = (totalBlocks_ > numSendCores_ + 1U) ? (totalBlocks_ - numSendCores_ - 1U) : 0U; if (numRecvCores_ == 0U) { numRecvCores_ = 1U; numSendCores_ = (totalBlocks_ > 1U) ? (totalBlocks_ - 1U) : 1U; // ← 本 PR 新增:totalBlocks_==1 时 numSendCores_=1 } isSender_ = (aivId_ < numSendCores_) || (totalBlocks_ <= 1U); isReceiver_ = (aivId_ >= numSendCores_ && aivId_ < totalBlocks_ - 1U) || (totalBlocks_ <= 1U); // totalBlocks_==1 时恒 true isFlagCore_ = (aivId_ == totalBlocks_ - 1U);__aicore__ inline void EngramFetchGradArch35::RecvGradFromPeers() { if (!isReceiver_) { return; } GM_ADDR localWinBase = (GM_ADDR)ctxPtr_->commBuffer[rankId_]; uint32_t recvIdx = aivId_ - numSendCores_; // ← aivId_=0, numSendCores_=1 时回绕为 0xFFFFFFFF uint32_t totalWorkUnits = (numRanks_ - 1U) * sendersPerRank_; if (totalWorkUnits == 0U) { return; } // Each work unit must be owned by EXACTLY one receiver core: with numRecvCores_ > // totalWorkUnits, `recvIdx % totalWorkUnits` would map several cores onto the same // (srcRank, senderIdx) unit and race on the tokenRead counters (flow-control slots // get reused while the slower duplicate is still copying -> recvGrad corruption). for (uint32_t wIdx = recvIdx; wIdx < totalWorkUnits; wIdx += numRecvCores_) { // ← 0xFFFFFFFF < totalWorkUnits 恒假,零迭代 uint32_t adjustedSrcRank = wIdx / sendersPerRank_;正向证据:
负向证据:
wIdx < totalWorkUnits兜住内存访问,后果限于功能失效而非越界自信值 = 40+30+20+15+10-20 = 95% ≥ 70% → 判定违规
uint32_t recvIdx = (aivId_ > numSendCores_) ? (aivId_ - numSendCores_) : 0U;;同时建议在 fallback 中保证至少有一个核的 recvIdx 落在[0, totalWorkUnits)内。[cpp-secure 4.1] 外部输入校验 —— rankSize 一致性校验 release 归零
rankSize一致性校验的强制力由"release 下降级为确定性挂死(RUNTIME_ABORT 的 while(true) 兜底)"改为纯ascendc_assert。已验证本算子构建不定义ASCENDC_DUMP,该断言在 release 产物中被预处理为空宏,外部输入numRanks_在发布形态下零有效校验。若 commContext 与 tiling 的 rankSize 不一致(两个独立来源,正是代码注释 :470-471 自证"必须一致性校验,否则…越界"),Kernel 将带非法值继续执行:numRanks_=0时在 :504sendersPerRank_ = halfBlocks / numRanks_、:544tokenSlotSize_ = tokenArea / numRanks_ / NUM_SLOTS触发设备侧除零 UB;numRanks_ > tilingData->rankSize时 displs 区(:570-572 按 numRanks_ 规划)、statusBuf/tempBuf/indicesBuf 尺寸全面偏离 Host 按 tilingData->rankSize 规划的 workspace → GM 越界写。旧代码两条路径(debug abort / release 挂死)均不会发生内存越界,本 PR 造成 release 侧安全回退。(原代码为aivId_ = GetBlockIdx(); totalBlocks_ = GetBlockNum(); tileBytes_ = TILE_BYTES; ctxPtr_ = (__gm__ EngramCommContext *)commContext; rankId_ = ctxPtr_->rankId; numRanks_ = ctxPtr_->rankSize; // commContext 为设备侧外部数据,与 Host 侧 tiling 的 rankSize(sendCounts.dim0/8)互为独立来源, // 必须一致性校验,否则 workspace 的 displs 区按 Host rankSize 规划而 Kernel 按 numRanks_ 写入会越界 ascendc_assert(numRanks_ != 0U && numRanks_ <= Mc2Kernel::MAX_QP_SIZE && numRanks_ == tilingData->rankSize, "invalid rankSize: commContext=%u, tiling=%u", numRanks_, tilingData->rankSize); channelsPerRank_ = ctxPtr_->channelsPerRank; if (channelsPerRank_ == 0) { channelsPerRank_ = 1; }if (numRanks_ == 0U || numRanks_ > MAX_QP_SIZE || numRanks_ != tilingData->rankSize) { RUNTIME_ABORT(...); },RUNTIME_ABORT 含while(true)release 兜底)正向证据:
负向证据:
自信值 = 40+20+15+15 = 90% ≥ 70% → 判定违规
ascendc_assert承载。建议改为显式分支:校验失败时置错误标志走安全退出路径(如 numRanks_ 无效时跳过通信阶段直接写零输出并 return),或恢复带确定性终止语义的兜底(保留 RUNTIME_ABORT 形态仅用于此类安全校验,与纯日志性 assert 区分)。[cpp-secure 1.2] 内存安全 —— RUNTIME_ABORT→ascendc_assert 后可达越界写
RUNTIME_ABORT(assert 后while(true)无条件死旋,release 下仍确定性挂死、内存安全)替换为ascendc_assert(release 空宏,已验证)。因此 shipping/release 构建下这些校验完全消失,失败后流程带着非法状态继续执行,产生可达的越界写。最严重一处:pendingHandles_固定数组(MAX_PENDING_HANDLES=8,声明于 :156)越界写——发送核 handle 数 = ⌈numRanks_/halfBlocks⌉(SendGradToPeers :859 步长 numSendCores_,SendGradRemote 每次 :957 追加 1 个),当 numRanks_ > 4×totalBlocks(Host 仅校验 rankSize ≤ 1024,tiling.cpp:276,无 rankSize 与核数关系约束;aivNum=48 时 rankSize ∈ (192,1024] 即触发)时 count 可达 ~43 ≫ 8,release 下 :956 直接写越界 280+ 字节,篡改相邻成员(isSender_/gradSortedGM_/recvGradGM_ 等 GM 指针)→ 后续任意 GM 写。次严重:numRanks_ 一致性校验失效(:472)后 displs 区写入越出 Host 规划的 workspace。对比 :474-477channelsPerRank_仍保留 release 生效的if回退——同函数内正确的防御范式反衬此处弱化。int32_t ret = hcomm_.WriteWithNotifyNbi(handle, remoteSlotAddr, srcAddr, dataBytes, remoteCounterAddr, static_cast<uint64_t>(localWriteCnt + 1)); ascendc_assert(ret == 0, "WriteWithNotifyNbi failed, ret=%d, tag=ExTok_data, rankId=%u, dstRank=%u", ret, rankId_, dstRank); localWriteCnt++; totalSent += chunkLen; } RetireCreditCounter(); // 单核 remote handle 数随 numRanks_/numSendCores_ 配置增长,必须守卫固定数组边界 ascendc_assert(pendingHandleCount_ < Mc2Kernel::MAX_PENDING_HANDLES, "pendingHandles overflow: count=%u, max=%u, rankId=%u, dstRank=%u", pendingHandleCount_, Mc2Kernel::MAX_PENDING_HANDLES, rankId_, dstRank); pendingHandles_[pendingHandleCount_] = handle; pendingHandleCount_++;ctxPtr_ = (__gm__ EngramCommContext *)commContext; rankId_ = ctxPtr_->rankId; numRanks_ = ctxPtr_->rankSize; // commContext 为设备侧外部数据,与 Host 侧 tiling 的 rankSize(sendCounts.dim0/8)互为独立来源, // 必须一致性校验,否则 workspace 的 displs 区按 Host rankSize 规划而 Kernel 按 numRanks_ 写入会越界 ascendc_assert(numRanks_ != 0U && numRanks_ <= Mc2Kernel::MAX_QP_SIZE && numRanks_ == tilingData->rankSize, "invalid rankSize: commContext=%u, tiling=%u", numRanks_, tilingData->rankSize); channelsPerRank_ = ctxPtr_->channelsPerRank; if (channelsPerRank_ == 0) { channelsPerRank_ = 1; }正向证据:
负向证据(核查后不计分):各失败路径下游均为挂死而非越界(TimeoutCheck/通信失败 4 类点 release 下为无限自旋挂死,与旧挂死等价);缓冲区收缩构造性安全(FlushAccum 借用区
castTailBytes == uniqueEntryBytes_,两侧公式一致)。自信值 = 40+30+20+15+15 = 120% ≥ 70% → 判定违规
while(true)挂死语义,不依赖ASCENDC_DUMP门控的ascendc_assert作唯一防线);或 Host 侧补齐对应约束:Tiling 校验 rankSize 与 aivNum 的上界关系(如⌈numRanks_/halfBlocks⌉ ≤ MAX_PENDING_HANDLES),并在 commContext 接入处前置校验 rankSize 一致性,使 assert 仅作调试辅助。[cpp-secure 1.3] 未定义行为 —— release 可达整数除零
ascendc_assert(:472)为空宏(已验证kernel_operator_dump_tensor_intf.h:65-68,且工程未定义ASCENDC_DUMP),设备侧外部数据numRanks_ = ctxPtr_->rankSize(:469,原 RUNTIME_ABORT 挂死路径已删除)未被拦截。当 commContext 中 rankSize 为 0 时,执行至 :544tokenArea / numRanks_ / NUM_SLOTS发生整数除零——ISO C++ [expr.mul] 明确规定的未定义行为;numRanks_ 非 0 但与 Host rankSize 不一致时,:976(numRanks_ - 1U) * sendersPerRank_回绕/超大值驱动接收循环对recvCountsGM_ + srcRank*4越界读。变更前while(true)无条件挂死使 :544 在 release 下不可达,本 PR 使其可达。uint64_t tokenAreaRaw = (winSize_ > tokenDataOffset_) ? (winSize_ - tokenDataOffset_) : 0U; uint64_t tokenArea = tokenAreaRaw; if (hiddenBytes_ > 0) { uint64_t hiddenBytes = static_cast<uint64_t>(hiddenBytes_); tokenArea = Ceil(tokenAreaRaw, hiddenBytes) * hiddenBytes; if (tokenArea > tokenAreaRaw) { tokenArea = (tokenAreaRaw / hiddenBytes) * hiddenBytes; } } tokenSlotSize_ = tokenArea / numRanks_ / NUM_SLOTS; maxTokensPerSlot_ = static_cast<uint32_t>(tokenSlotSize_ / static_cast<uint64_t>(hiddenBytes_)); if (maxTokensPerSlot_ == 0U) { maxTokensPerSlot_ = 1U; }正向证据:
__gm__ EngramCommContext设备侧外部内存,与 Host 独立自信值 = 40+30+20+15 = 105% ≥ 70% → 判定违规
tokenSlotSize_ = (numRanks_ != 0U) ? tokenArea / numRanks_ / NUM_SLOTS : 0U;并对 numRanks_ 非法时的后续流程短路),或将 numRanks_ 合法性校验还原为不依赖ASCENDC_DUMP的确定性终止,确保 release 构建下除法不可达。[cpp-general 12.1] 断言 —— 运行期错误仅用断言校验且删除既有错误处理
ascendc_assert直报,属「运行期错误仅用断言校验」的条款禁止模式,且语义实质性弱化:① release 下防护归零(CANN 9.0.0 头文件实证空宏,项目无 ASCENDC_DUMP/NDEBUG 定义);② 删除了真实错误处理——WriteNbiChecked(:197)/DrainChecked(:203)/WriteWithNotifyNbi(:944) 三处原含 3 次重试,本 PR 连重试一并删除,通信失败在 release 下被静默忽略 → 数据未发出/对端计数器不更新 → 对端自旋,而超时兜底 TimeoutCheck 同为空宏 → 全作业无诊断静默挂死或脏数据继续(旧代码为确定性 fail-stop);③ 内存安全守卫失效(rankSize 一致性 :472、pendingHandles :953、UB 池授权 :632)。__aicore__ inline void EngramFetchGradArch35::WriteNbiChecked(uint64_t handle, GM_ADDR dst, GM_ADDR src, uint64_t len) { int32_t ret = hcomm_.WriteNbi(handle, dst, src, len); ascendc_assert(ret == 0, "WriteNbi failed, ret=%d, rankId=%u, aivId=%u", ret, rankId_, aivId_); } __aicore__ inline void EngramFetchGradArch35::DrainChecked(uint64_t handle) { int32_t ret = hcomm_.Drain(handle); ascendc_assert(ret == 0, "Drain failed, ret=%d, rankId=%u, aivId=%u", ret, rankId_, aivId_); } __aicore__ inline void EngramFetchGradArch35::TimeoutCheck(uint64_t startTime, TimeoutSite site) { uint64_t nowUs = static_cast<uint64_t>(AscendC::GetSystemCycle()) / ENGRAM_GRAD_CYCLES_PER_US; ascendc_assert((nowUs - startTime) < ENGRAM_GRAD_TIMEOUT_US, "timeout, tag=%d, rankId=%u, aivId=%u, elapsed=%llu us\n", static_cast<int>(site), rankId_, aivId_, nowUs - startTime); }正向证据:
负向证据:
自信值 = 40+30+20+15+15+10-20-15 = 95% ≥ 70% → 判定违规(≥80% → HIGH)
while(true)自旋 + 可选 PRINTF,而非依赖ASCENDC_DUMP门控的调试断言);② 影响内存安全/全局一致性的校验(:472 rankSize 一致性、:953 pendingHandles 越界、:632 UB 池授权)定义项目级ALWAYS_ACTIVE_CHECK宏(不满足即确定性自旋);③ TimeoutCheck 若接受「超时即不可恢复」语义,应显式 fail-stop 自旋并保留 site 标记。[cpp-general 1.1] 外部数据合法性 —— commContext 外部数据 release 无校验
ctxPtr_->rankSize(来自__gm__ EngramCommContext,代码注释自证其为与 Host tiling "互为独立来源" 的外部输入)的唯一一致性校验由if(...) RUNTIME_ABORT(...)改写为裸ascendc_assert。release/生产构建下该外部数据完全无校验直接投入使用:numRanks_直接参与除法sendersPerRank_ = halfBlocks / numRanks_(:504)、tokenSlotSize_ = tokenArea / numRanks_ / NUM_SLOTS(:544)及 workspace 窗口偏移规划(:528-533)——与 Host 侧规划的 displs 区不一致时越界写,rankSize==0时设备侧除零。旧代码在 release 下if条件是真实生效的判断(失败降级为挂死,数据不会被非法使用),本 PR 将if整体删除后,该防线在 release 下不复存在,属外部数据校验的真实回退。注意:其余被改写点(FlushAccum castTailBytes、UB 池 :632、pendingHandleCount_ :953)均有覆盖同一变量的 Host 侧上游校验(tiling.cpp:615、新增 :669/:684、:697-702),双层防护成立,不构成本条例违规;hcomm 返回码/超时检查不属于"外部数据"范畴,不在本条例边界内。aivId_ = GetBlockIdx(); totalBlocks_ = GetBlockNum(); tileBytes_ = TILE_BYTES; ctxPtr_ = (__gm__ EngramCommContext *)commContext; rankId_ = ctxPtr_->rankId; numRanks_ = ctxPtr_->rankSize; // commContext 为设备侧外部数据,与 Host 侧 tiling 的 rankSize(sendCounts.dim0/8)互为独立来源, // 必须一致性校验,否则 workspace 的 displs 区按 Host rankSize 规划而 Kernel 按 numRanks_ 写入会越界 ascendc_assert(numRanks_ != 0U && numRanks_ <= Mc2Kernel::MAX_QP_SIZE && numRanks_ == tilingData->rankSize, "invalid rankSize: commContext=%u, tiling=%u", numRanks_, tilingData->rankSize); channelsPerRank_ = ctxPtr_->channelsPerRank;正向证据:
负向证据:
自信值 = 40+30+20+15+15+10-20 = 110% ≥ 70% → 判定违规
if运行时判断,失败走确定性失败路径(如向状态/输出 GM 写入错误标记后短路后续通信与计算阶段,或至少恢复旧if(...) { ... }判定结构),不得将跨源一致性校验唯一寄托于仅调试构建生效的ascendc_assert;其余纯内部预算自检点(有 Host 双层防护的)可保留 assert 形态。需关注(MED 置信度)
[cpp-secure 2.3] 除零保护 SUSPICIOUS(75%,需人工确认)
sendersPerRank_ = halfBlocks / numRanks_;、:544tokenSlotSize_ = tokenArea / numRanks_ / NUM_SLOTS;)的除数来自设备侧外部数据 commContext。唯一守卫是本 PR 由 RUNTIME_ABORT 改写的ascendc_assert(:472-473)——release 编译为空宏,而旧 RUNTIME_ABORT 的while(true)死循环在 release 下仍生效。即:release 构建中本 PR 移除了对外部输入 numRanks_==0 的最后屏障,畸形 commContext 将直达除法产生除零 UB。按条例守卫模式表,ASSERT 类守卫仅在 moe/ 族视为有效,本算子属 mc2/,条款自身规定该场景降级为 SUSPICIOUS。其余除法均安全:alignUb/tmpAlignedCount 除数为 constexpr 32;availableForCast/(fp32RowStride*ACCUM_BUF_COPIES)(tiling.cpp:693)除数 fp32RowStride ≥ 32 由 Host :231 hiddenDim>0 上游保证;availableForCast 减法链由本 PR 新增的 minUniqueNeed 校验(:684-690)先行兜底。除法行本身未变更(暴露为存量),防线弱化由 PR 引入。if (numRanks_ == 0U || numRanks_ > MAX_QP_SIZE || ...) { 置错误状态并短路 Process; return; }),或保留一个 release 下仍编译的确定性兜底,再叠加 ascendc_assert 用于调试定位。