感谢提供完整的复现代码和对照结果。您报告的关键差异是:在 Atlas A2、CANN 9.2.0 环境中,声明 epWorldSize=8、实际 EP 通信域为 2 rank 时,两段接口均返回 0,但输出保留 0xA5;相同参数在真实 8 rank 通信域中能够正常输出。这为排查通信域规模与算子参数不一致提供了明确线索。V1/V3/V4 的同类现象,以及 epWorldSize=16、实际 8 rank 时的挂死,目前按您提供的实测情况记录。
问题分析
这里需要区分接口返回成功与设备侧执行成功。复现代码调用了 aclrtSynchronizeStream,但没有检查其返回值,随后读取输出的 aclrtMemcpy 返回值也未检查。因此,两段返回 0 仍不足以区分“执行结束但未写输出”和“异步执行失败、同步或回读发生异常”。这不影响您反馈的问题值得排查,但会影响对静默成功机制的判断。
目前的证据还不能确定校验具体缺失在哪一层,也不能将挂死直接归因于等待不存在的 rank。声明的通信规模与实际通信域不一致,和通信域规模正确但部分进程未参与执行,也需要分别处理;后者不能仅靠比较 rank 数发现。
处理方向
一个可行方向是在能够取得实际 EP 通信域信息的主机侧入口,将其 rank 数与 epWorldSize 比较,不一致时提前返回错误,避免继续提交设备任务。如果第一段已具备查询条件,放在第一段拒绝更有利于调用方及时发现配置错误。具体是否返回您建议的 561002,还需结合各版本公开接口的错误码定义及底层错误传递方式确认。
当前可先在调用侧检查实际 EP 通信域大小与 epWorldSize 一致,并在同步、输出回读失败时停止使用输出,避免继续传给 combine/FFN。请补充现有复现中 aclrtSynchronizeStream 和输出回读 aclrtMemcpy 的返回码;若有失败,再附对应的运行时错误日志片段即可。这些信息有助于区分异步失败与执行完成后未写输出,无需为补充信息再次运行已知会挂死的配置。


/assign @luobaiqing


问题描述
aclnnMoeDistributeDispatchV2在声明的epWorldSize大于实际 EP 通信域 rank 数时:aclnnMoeDistributeDispatchV2GetWorkspaceSize返回0,第二段也返回0;expandX、epRecvCounts、expertTokenNums、tpRecvCounts全部保持调用前的内容——输出 buffer 预填0xA5金丝雀,执行后仍是0xA5,一个字节都没被写过;epWorldSize=16而只起 8 个 rank,算子挂死(进程进入D状态,kill -9无效)。同一现场在
aclnnMoeDistributeDispatch/V3/V4上一致复现(原始日志见下方"实测输出")。环境信息
cann/ops-transformer,源码核对点origin/master@4dc31cf4/home/l00797864/cann/ascend-toolkit)HcclCommInitRootInfo(2, ...),即真实 EP 通信域只有 2 个 rank复现步骤
双卡 2-rank 起通信域,声明
epWorldSize=8(资料 A2/V2 明确支持 8),moeExpertNum=8,commAlg="fullmesh",BS=8 / H=512 / K=1,输出 buffer 预填0xA5后调用两段接口:ep=8+ 真实 8 rank 时同参数可正常输出(逐字节校验通过),说明不是形状/属性本身非法,问题在"声明与实际通信域不一致时无校验"。moe_disp_probe侧同现场(epRecvCounts/expertTokenNums也全是0xA5A5A5A5对应的-1515870811/-6510615555426900571):复现代码(自包含,
mode=mismatch)#include <cstdio> #include <cstring> #include <thread> #include <vector> #include "acl/acl.h" #include "hccl/hccl.h" #include "aclnnop/aclnn_moe_distribute_dispatch_v2.h" static HcclRootInfo g_root; static char g_group[128] = {0}; static aclTensor *Make(std::vector<int64_t> sh, aclDataType dt, size_t bytes, void **dev, bool canary = false) { aclrtMalloc(dev, bytes, ACL_MEM_MALLOC_HUGE_FIRST); if (canary) { // 金丝雀:算子写过任何字节都能看出来 std::vector<unsigned char> pat(bytes, 0xA5); aclrtMemcpy(*dev, bytes, pat.data(), bytes, ACL_MEMCPY_HOST_TO_DEVICE); } std::vector<int64_t> st(sh.size(), 1); for (int64_t i = (int64_t)sh.size() - 2; i >= 0; --i) st[i] = sh[i + 1] * st[i + 1]; return aclCreateTensor(sh.data(), sh.size(), dt, st.data(), 0, ACL_FORMAT_ND, sh.data(), sh.size(), *dev); } static void RankRun(int rank) { aclrtSetDevice(rank); aclrtContext ctx = nullptr; aclrtCreateContext(&ctx, rank); aclrtStream stream = nullptr; aclrtCreateStream(&stream); HcclComm comm = nullptr; if (HcclCommInitRootInfo(2, &g_root, rank, &comm) != HCCL_SUCCESS) return; // 真实通信域只有 2 个 rank char name[128] = {0}; HcclGetCommName(comm, name); if (!g_group[0]) snprintf(g_group, sizeof(g_group), "%s", name); const int BS = 8, H = 512, K = 1, EP = 8, MOE = 8, A = BS * EP; // 声明 epWorldSize=8 std::vector<int16_t> xh((size_t)BS * H, 0x3C00); std::vector<int32_t> idh((size_t)BS * K, 0); for (int i = 0; i < BS; ++i) idh[i] = (rank * BS + i) % MOE; void *px = nullptr, *pid = nullptr, *pes = nullptr, *pout = nullptr, *pdyn = nullptr, *pas = nullptr; void *petn = nullptr, *pep = nullptr, *ptp = nullptr, *pexs = nullptr; aclTensor *x = Make({BS, H}, ACL_FLOAT16, (size_t)BS * H * 2, &px); aclTensor *ids = Make({BS, K}, ACL_INT32, (size_t)BS * K * 4, &pid); aclrtMemcpy(px, (size_t)BS * H * 2, xh.data(), (size_t)BS * H * 2, ACL_MEMCPY_HOST_TO_DEVICE); aclrtMemcpy(pid, idh.size() * 4, idh.data(), idh.size() * 4, ACL_MEMCPY_HOST_TO_DEVICE); aclTensor *eScales = Make({BS, K}, ACL_FLOAT, (size_t)BS * K * 4, &pes); aclTensor *expandX = Make({A, H}, ACL_FLOAT16, (size_t)A * H * 2, &pout, true); aclTensor *dyn = Make({A}, ACL_FLOAT, (size_t)A * 4, &pdyn, true); aclTensor *assist = Make({(int64_t)A * 128}, ACL_INT32, (size_t)A * 128 * 4, &pas, true); aclTensor *etn = Make({MOE / EP}, ACL_INT64, (size_t)(MOE / EP) * 8, &petn, true); aclTensor *epRecv = Make({MOE}, ACL_INT32, (size_t)MOE * 4, &pep, true); aclTensor *tpRecv = Make({1}, ACL_INT32, 4, &ptp, true); aclTensor *exScales = Make({A}, ACL_FLOAT, (size_t)A * 4, &pexs, true); uint64_t ws = 0; aclOpExecutor *exec = nullptr; aclnnStatus rc1 = aclnnMoeDistributeDispatchV2GetWorkspaceSize( x, ids, nullptr, nullptr, eScales, g_group, EP, rank, MOE, "", 0, 0, 0, 0, 0, 0, BS * EP, 0, "fullmesh", expandX, dyn, assist, etn, epRecv, tpRecv, exScales, &ws, &exec); int rc2 = -999; if (rc1 == 0) { void *wsDev = nullptr; if (ws > 0) aclrtMalloc(&wsDev, ws, ACL_MEM_MALLOC_HUGE_FIRST); rc2 = (int)aclnnMoeDistributeDispatchV2(wsDev, ws, exec, stream); aclrtSynchronizeStream(stream); } unsigned char head[8] = {0}; aclrtMemcpy(head, 8, pout, 8, ACL_MEMCPY_DEVICE_TO_HOST); bool canary = true; for (unsigned char c : head) if (c != 0xA5) canary = false; printf("[rank %d] rc1=%d rc2=%d expandX[0..7]=%02X%02X%02X%02X%02X%02X%02X%02X %s\n", rank, (int)rc1, rc2, head[0], head[1], head[2], head[3], head[4], head[5], head[6], head[7], canary ? "(输出未写出,仍是 0xA5)" : "(已写出)"); HcclCommDestroy(comm); } int main() { aclInit(nullptr); aclrtSetDevice(0); aclrtContext c0 = nullptr; aclrtCreateContext(&c0, 0); if (HcclGetRootInfo(&g_root) != HCCL_SUCCESS) return 1; std::thread t0(RankRun, 0), t1(RankRun, 1); t0.join(); t1.join(); aclFinalize(); return 0; }编译运行:
g++ -std=c++17 repro.cpp -o repro -pthread -I$ASCEND_HOME_PATH/include -L$ASCEND_HOME_PATH/lib64 \ -lascendcl -lhccl -lnnopbase -lopapi -lacl_op_compiler ASCEND_RT_VISIBLE_DEVICES=4,5 ./repro期望行为
第一段接口应对
epWorldSize与实际 EP 通信域 rank 数做交叉校验,不一致时返回561002(参数取值不在支持范围);至少不能"返回 0 且输出未写",否则调用方会拿未初始化显存当 token 特征继续算 combine/FFN。
影响
P1:静默成功 + 输出为未初始化显存。典型触发方式是启动的 rank 数少于
epWorldSize(漏起进程、扩缩容、调度失败),调用侧只看返回码无法发现,错误会以"特征全为脏数据"的形式扩散到下游,且难以定位。
其他
epWorldSize=16+ 8 rank 时进程进入D状态、kill -9无效,会占用 device 上下文,需要等待设备侧 I/O 自行返回。