Open
[Bug] dense_lightning_indexer_grad_kl_loss(TND)偶发巨大 loss/grad —— MindSpore aclnn 缓存哈希未包含 actual_seq 数值 #2
hedongdongAdd on Jun 30
Jun 30 Modify description of issue
Jun 30 Modify description of issue
Hhedongdong
Jun 30 Change title to "[Bug] dense_lightning_indexer_grad_kl_loss(TND)偶发巨大 loss/grad —— MindSpore aclnn 缓存哈希未包含 actual_seq 数值", originally titled "[Bug] dense_lightning_indexer_grad_kl_loss 偶发巨大 loss/grad —— aclnn executor 缓存复用下算子 repeatable 不安全".
Jun 30 Change title to "[Bug] dense_lightning_indexer_grad_kl_loss(TND)偶发巨大 loss/grad —— MindSpore aclnn 缓存哈希未包含 actual_seq 数值", originally titled "[Bug] dense_lightning_indexer_grad_kl_loss 偶发巨大 loss/grad —— aclnn executor 缓存复用下算子 repeatable 不安全".
Jun 30 Modify description of issue
Jun 30 Modify description of issue
Jun 30 Modify description of issue
Jul 2 Associated pull request
Jul 3 Associated pull request
[Bug] dense_lightning_indexer_grad_kl_loss(TND)偶发巨大 loss/grad —— MindSpore aclnn 缓存哈希未包含 actual_seq 数值
⚠️ 更正声明(撤回此前的 CANN 归因)
本 issue 早前版本曾判断"CANN 算子
aclnnDenseLightningIndexerGradKLLossrepeatable 不安全"。该结论经实验证伪、已撤回:aclSetAclOpExecutorRepeatable+aclSetInput/OutputTensorAddr刷新地址后复用同一 executor,结果与全新构建完全一致(reused == golden)。算子是 repeatable-safe 的。UpdateAddress索引方案一致 —— MindSpore 的地址刷新索引也是对的。真因不在地址刷新,而在缓存哈希,见下。
1. 问题现象
DeepSeek 类模型 TND 训练中,
npu_dense_lightning_indexer_grad_kl_loss在 step ~380 之后偶发巨大indexer_loss/梯度,随训练增长;pynative_synchronize全程开启;离线单算子复算正确;关闭 aclnn 缓存(aclnn_cache_queue_length:0)异常完全消失;关输出清零/recompute/VMM/memory_recycle 均无效。2. 根因
actual_seq_qlen/klen在自定义算子封装里以std::pair<std::vector<int64_t>, bool>传入LAUNCH_ACLNN,参与 executor 缓存的AclnnHash。但 MindSpore 的哈希对该类型没有专门重载:op_api_cache.cc只有GatherInfo/GatherHash(const std::pair<KernelTensor*, bool>&);std::pair<std::vector<int64_t>, bool>版本(而ConvertType/GetAddr都为它写了专门重载,唯独哈希漏了)。于是落到通用模板
GatherInfo(const T&)→MemcpyToBuf(&pair, sizeof(pair)),只哈希 vector 的{data_ptr, size, capacity}+ bool 这几个字节,不哈希 vector 指向的数值内容。后果:
actual_seq数值不进哈希。当某次调用的actual_seqvector 堆缓冲被分配器回收到与某个"段数相同(vector.size() 相同)、但数值不同"的陈旧缓存条目相同的地址时,两者哈希字节完全相同 → 哈希碰撞 → 复用为别的 packing 构建的 executor → 序列边界(tiling/mask)错 → loss/grad 爆。随训练堆状态与 1024 LRU 缓存演化,碰撞概率上升 → "step380+、随缓存填满越来越多"。actual_seq每次 vector 指针不同 → 哈希每次不同 → 该算子缓存几乎从不命中(每次重建 executor)。各现象自洽:
actual_seq非空)触发;BSND 传None(空 vector)无内容碰撞,从不复现。3. 验证(均已实测)
acl_ir仅有pair<KernelTensor*,bool>的哈希重载,无pair<vector<int64_t>,bool>版。actual_seq=[32,128],内容完全相同)连调 6 次,miss cache, with hash id打出 6 个互不相同的 hash_id(14335366055291193910 / 15133310985169689141 / 14093785868291482850 / 8239392667452964647 / 4728391658603128516 / 481978824137881733)。内容相同而 hash 每次变 ⟹ 哈希按 vector 指针算、不含内容。aclnn_cache_queue_length:0消除。3.5 对比 PTA(torch_npu):为何同架构却无此问题
PTA 用的是同一套机制(PTACache + libopapi 的 repeatable executor),但不会碰这个 bug,差别恰恰在哈希对 int array 的处理:
PTA 为
actual_seq的类型(at::IntArrayRef/at::ArrayRef<c10::SymInt>/c10::OptionalArrayRef<c10::SymInt>)写了专门的add_param_to_buf重载,按内容哈希:// op-plugin/op_plugin/utils/op_api_common.cpp void add_param_to_buf(const at::IntArrayRef &at_array) { MEMCPY_TO_BUF(at_array.data(), at_array.size() * sizeof(int64_t)); // 哈希【内容】 auto counter = at_array.size(); MEMCPY_TO_BUF(&counter, sizeof(counter)); // + 元素个数 } void add_param_to_buf(const at::ArrayRef<c10::SymInt> &int_array) { // actual_seq 实际走到这 auto at_array = c10::asIntArrayRefUnchecked(int_array); MEMCPY_TO_BUF(at_array.data(), at_array.size() * sizeof(int64_t)); ... } void add_param_to_buf(const c10::OptionalArrayRef<c10::SymInt> &opt) { // 即 actual_seq 类型 if (opt.has_value()) add_param_to_buf(opt.value()); else MEMCPY_TO_BUF(",", 1); }对比一目了然(同架构,差别就在这一个重载):
IntArrayRef/SymInt重载 → 按内容pair<vector<int64_t>,bool>重载 → 落通用模板按指针PTA 验证:① 日志坐实命中缓存复用同一 executor;② BSND 4000+ 次稳定;③ 在与 MindSpore 完全相同的触发条件下(TND + 逐次变化 actual_seq + 持续填充缓存)实测 4 golden × 600 轮 0 异常、纹丝不动 —— 而同条件下 MindSpore iter 120 起偏离、峰值相对偏差 114%。同一触发条件 MindSpore 坏、PTA 好,差别即上面这个哈希重载。MindSpore 的修复就是把该重载补齐、对齐 PTA 的做法。
4. 修复方案
MindSpore 框架(
mindspore/ops/kernel/ascend/acl_ir/op_api_cache):为actual_seq的类型补哈希重载,按 vector 内容哈希(复用已有的GatherInfo(const std::vector<T>&)):inline void GatherInfo(const std::pair<std::vector<int64_t>, bool> &p) { GatherInfo(p.first); // = MemcpyToBuf(data, size*sizeof(int64_t)),按内容哈希 MemcpyToBuf(&p.second, sizeof(bool)); } inline void GatherInfo(const std::pair<std::optional<std::vector<int64_t>>, bool> &p) { if (p.first.has_value()) GatherInfo(p.first.value()); MemcpyToBuf(&p.second, sizeof(bool)); }效果:
actual_seq数值进哈希 → 不同 seq 不同 hash → 消除碰撞(修正确性)+ 同 seq 稳定命中(修性能)。该修复对所有以pair<vector<int64_t>,bool>传值依赖 int array 的自定义算子通用。过渡 workaround:把该算子封装改成手写 cache-free runner(对齐 sparse/softmax_lse 两个兄弟),绕开缓存即绕开碰撞;在框架修复前可先用,代价是该算子放弃 executor 缓存。
5. 关键引用
mindspore/ops/kernel/ascend/acl_ir/op_api_cache.h:102-105(通用GatherInfo=MemcpyToBuf(&value,sizeof(T)))、:123-126(vector<T>按内容哈希,可复用)、:236-250(GatherHash重载列表,无 pairmindspore/ops/kernel/ascend/acl_ir/op_api_cache.cc:128,425(仅pair<KernelTensor*,bool>哈希重载)mindspore/ops/kernel/ascend/acl_ir/op_api_convert.h:393(ConvertType有pair<vector<int64_t>,bool>版)、:603(GetAddr有,唯独哈希漏)hyper_parallel/platform/mindspore/custom_ops/dense_lightning_indexer_grad_kl_loss.cc:51-52(make_pair(vector, true)传actual_seq)op-plugin/op_plugin/utils/op_api_common_base.h:301-309(add_param_to_buf的IntArrayRef/SymInt/OptionalArrayRef重载声明)、op_api_common.cpp(实现:MEMCPY_TO_BUF(at_array.data(), size*sizeof(int64_t))按内容哈希)