AscendC 910B GM 标量/MTE 双向缓存不一致 Bug 详解一句话总结在 910B (DAV_2201) 芯片上同一块 GM 显存地址标量赋值gmPtr[i] v和 DMA 搬运DataCopy之间没有硬件缓存一致性协议。两个方向都可能写丢或读错精度误差会膨胀10~100 倍。1. 背景910B 的两条内存通道AscendC 的 AICore 访问 GM显存时其实有两条独立的通路┌──────────────────────────────────────────────────────────┐ │ AICore │ │ │ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │ │ 标量通路 (DataCache) │ │ MTE 通路 (DMA) │ │ │ │ │ │ │ │ │ │ gmPtr[i] val │ │ DataCopy / │ │ │ │ gmPtr[i] val │ │ DataCopyPad │ │ │ │ gmPtr[i] │ │ │ │ │ └──────────┬───────────┘ └──────────┬───────────┘ │ │ │ │ │ └──────────────┼─────────────────────────────┼──────────────┘ │ │ ▼ ▼ ┌─────────────────────────────────────────┐ │ GM (Global Memory) │ └─────────────────────────────────────────┘ ▲ ▲ │ │ 没有一致性协议一个人写的东西另一个人不一定看得到CPU 上有 MESI 等缓存一致性协议硬件帮你通知对端刷缓存。910B 上没有 —— 这两条路互相不可见。2. 什么是标量访问 vs “MTE 搬运”维度标量访问MTE 搬运写法gmPtr[i] 1.0f;DataCopy(dst, src, len);通路走 DataCache走 DMA 引擎粒度单个元素float/int一整块连续内存适合场景少量、零散操作大块、批量搬运是否进 cache是DataCache否直通 DRAM简单说标量写 你往一个小信箱DataCache里塞纸条等攒够了一批才统一寄出去MTE 搬运 叫搬运工DMA一次性把一车货从仓库搬到工作台问题来了小信箱和搬运工之间没有对讲机。你塞进去的纸条搬运工不一定知道搬运工刚搬走的东西你的小信箱里可能还留着旧纸条。3. 生活中的比喻想象你和小李合租一个仓库你 标量通路写完只在自己的小本子DataCache上记一笔小李 MTE 搬运工只看仓库门牌DRAM从不翻你的小本子场景 A你先记小李后搬1. 你把还剩 5 个箱子写在自己的小本子上 ← DataCache 里有新值 2. 小李按门牌去仓库搬货 ← MTE 读 DRAM看到的是旧值 3 3. 你的小本子和仓库不一致了场景 B小李先搬你后记1. 小李刚把仓库里3 个箱子改成0 个 ← MTE 写 DRAM异步还在路上 2. 你在仓库门牌上写还剩 8 个 ← 标量写覆盖了 DRAM 3. 几秒后小李的 DMA 到了把你的8盖成0 ← 你的写丢了两个方向都会出错4. Bug 的两个具体方向方向 1标量写 → MTE 读// 标量写把 computed_value 写到 GM 缓冲区__gm__float*dSF32/* GM scratch */;for(uint32_ti0;iN;i){dSF32[i]computed_value;// 进了 DataCache不一定到 DRAM}// MTE 读把同一块 GM 搬到 UBDataCopy(ubBuf,dSF32,N);// DMA 直读 DRAM看不到 DataCache → 读到旧值方向 2MTE 写 → 标量写// MTE 写把工作台上的 zeroBuf 搬到 GM异步DataCopy(dWacc,zeroBuf,V*H);// DMA 还在路上// 标量写在同一地址上累加for(uint32_ti0;iV*H;i){dWacc[i]partial_sum;// 你的写可能被迟到的 DMA 盖掉}症状精度误差在1e-3 ~ 2e-2级别FP16 正常误差约 1e-4没有任何编译/运行报错只是结果不对。5. 简单复现代码host 端模拟下面这段独立可编译的 C 代码模拟了910B 的两条不互通通路在标准 CPU 上也能看到类似现象。它不是 AscendC 代码但用最少的代码把双向不一致这件事演示清楚// simulate_910b_incoherence.cpp// 编译g -stdc17 -O2 simulate_910b_incoherence.cpp -o sim ./sim//// 模拟 910B 上标量通路和DMA 通路共享同一块 GM// 但两边没有缓存一致性协议。#includecstdio#includecstring#includevector// 模拟标量通路的小本子DataCachestaticfloatg_scalar_notebook[16]{0};// 模拟GM 仓库DRAM刚开始是 0staticfloatg_gm[16]{0};// 模拟MTE 搬运工看到的 DRAM 视图staticfloatg_dma_view[16]{0};// 模拟标量通路把值写进小本子但不一定立刻同步到 GMvoidscalar_write(inti,floatv){g_scalar_notebook[i]v;// 910B 上这一步只是写 DataCacheDRAM 还没收到g_gm[i]v;// 模拟已同步到 DRAM —— 但实际硬件不保证}// 模拟 MTE 搬运工直接读 DRAM完全不知道小本子的存在voidmte_read_all(){memcpy(g_dma_view,g_gm,sizeof(g_gm));}// 模拟 MTE 写搬运工直接把一车零倒进 GMvoidmte_write_zeros(){// 标量通路可能不知道搬运工正在路上memset(g_gm,0,sizeof(g_gm));// 910B 上这是异步 DMA标量通路的小本子里仍是旧值g_scalar_notebook[0]42.0f;// 标量写把自己小本子改了// 如果 DMA 比这个标量写晚到标量写就被覆盖}intmain(){// 方向 1标量写 → MTE 读 printf( 方向 1标量写 - MTE 读 \n);for(inti0;i8;i)scalar_write(i,(float)(i1));// 假设标量通路忘了刷回 DataCacheMTE 只看到旧值// 我们手动把未同步状态模拟出来让 g_gm 保持为 0memset(g_gm,0,sizeof(g_gm));// 模拟 DRAM 实际还是旧值mte_read_all();printf(标量写的期望值: 1 2 3 4 5 6 7 8\n);printf(MTE 读到的实际: );for(inti0;i8;i)printf(%.0f ,g_dma_view[i]);printf( ← 全是旧值\n\n);// 方向 2MTE 写 → 标量写 printf( 方向 2MTE 写 - 标量写 \n);mte_write_zeros();// 搬运工把 GM 清零// 标量通路以为自己在 g_gm[0] 上写了 42但迟到的 DMA 可能盖掉// 我们模拟搬运工迟到把 g_gm[0] 改回 0g_gm[0]0.0f;// 模拟迟到的 DMA 写到达printf(标量写期望 g_gm[0] 42\n);printf(实际 g_gm[0] %.0f ← 被 DMA 覆盖了\n,g_gm[0]);return0;}运行结果标准 Linux 上即可复现这个两个方向都不一致的演示 方向 1标量写 - MTE 读 标量写的期望值: 1 2 3 4 5 6 7 8 MTE 读到的实际: 0 0 0 0 0 0 0 0 ← 全是旧值 方向 2MTE 写 - 标量写 标量写期望 g_gm[0] 42 实际 g_gm[0] 0 ← 被 DMA 覆盖了真实 910B 上是硬件帮你复制粘贴了这段故事DataCache 和 DMA 通路对同一地址的写入时序是不确定的谁最后到 DRAM 谁就赢。6. 真实 AscendC 代码长什么样❌ 错误写法触发 bug// kernel 内在 GM scratch 上做中间累加__gm__float*dSF32/* GM scratch */;// 方向 1标量写 GMfor(uint32_ti0;iN;i){dSF32[i]computed_value;// ← 写 DataCache}// 方向 1 后续MTE 读同一块 GMDataCopy(ubBuf,dSF32,N);// ← DMA 看不到 DataCache 的新值// —— 或者 ——// 方向 2MTE 写 GMDataCopy(dWacc,zeroBuf,V*H);// ← 异步 DMA// 方向 2 后续标量写同一地址for(uint32_ti0;iV*H;i){dWacc[i]partial_sum;// ← 可能被迟到的 DMA 覆盖}✅ 正确写法三种策略任选一种策略 1推荐在 UB 里完成所有中间计算根本不碰 GMTPipe ep;TBufTPosition::VECINeb;ep.InitBuffer(eb,ubSize);LocalTensorfloatubBufeb.Getfloat(N);// 全程在 UB 中计算for(uint32_ti0;iN;i){ubBuf.SetValue(i,computed_value);}// 最后一次性 DataCopy 到 GMDataCopy(gmOut,ubBuf,N);策略 2全程用标量访问不混 MTE// 清零标量写for(uint32_ti0;iV*H;i){dWacc[i]0.0f;}// 累加也是标量写同一通路 → 一致for(uint32_ti0;iV*H;i){dWacc[i]partial;}策略 3标量写后显式刷 DataCacheGlobalTensorDTgScratch;gScratch.SetGlobalBuffer((__gm__ DT*)scratch);// 标量写for(uint32_ti0;iN;i){gScratch.SetValue(i,(DT)computed_value);}// 显式刷回 DRAMDataCacheCleanAndInvalidDT,CacheLine::ENTIRE_DATA_CACHE(gScratch);// 现在 MTE 能读到一致的值DataCopy(ubBuf,gScratch,alignedN);7. 修复效果验证项修复前修复后改善Mode B grad_input 误差标量→MTE2.80e-31.53e-5183xBT edge tile grad_input 误差MTE→标量1.65e-22.44e-468xMode A 精度不受影响不受影响回归 OK误差从 1e-2 级别压到 1e-4~1e-5回到 FP16 的正常精度。8. 教训总结要点说明同一块 GM 只能走一种通路要么全程标量gmPtr[i]v要么全程DataCopyUB-only 中间计算是最优解既避免一致性陷阱又省 GM 带宽DataCacheCleanAndInvalid 是兜底实在要在 GM 上混用必须显式刷910B ≠ CPUCPU 有 MESI 自动帮你同步910B 没有症状很迷惑编译能过、运行不报错只是精度莫名变差 10~100 倍小 shape 更容易暴露BT4、V8 这种小规模反而最常触发附录什么时候应该怀疑这个 bug如果你看到以下任意一条先停下来检查代码里有没有 GM 上的标量/MTE 混用精度误差在1e-3 ~ 1e-2FP16 正常 ~1e-4同样的代码逻辑在 910A / 950 上没问题只在 910B 上飘消除 GM 中间缓冲后精度恢复正常gmPtr[i] v和DataCopy(..., gmPtr, ...)出现在同一地址没有编译错误、没有运行错误只是结果不对满足其中 2~3 条基本就是这个问题。改成 UB-only 中间计算立竿见影。
【昇腾/AscendC开发】AscendC 910B GM 标量/MTE 双向缓存不一致 Bug 详解
发布时间:2026/6/24 9:25:03
AscendC 910B GM 标量/MTE 双向缓存不一致 Bug 详解一句话总结在 910B (DAV_2201) 芯片上同一块 GM 显存地址标量赋值gmPtr[i] v和 DMA 搬运DataCopy之间没有硬件缓存一致性协议。两个方向都可能写丢或读错精度误差会膨胀10~100 倍。1. 背景910B 的两条内存通道AscendC 的 AICore 访问 GM显存时其实有两条独立的通路┌──────────────────────────────────────────────────────────┐ │ AICore │ │ │ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │ │ 标量通路 (DataCache) │ │ MTE 通路 (DMA) │ │ │ │ │ │ │ │ │ │ gmPtr[i] val │ │ DataCopy / │ │ │ │ gmPtr[i] val │ │ DataCopyPad │ │ │ │ gmPtr[i] │ │ │ │ │ └──────────┬───────────┘ └──────────┬───────────┘ │ │ │ │ │ └──────────────┼─────────────────────────────┼──────────────┘ │ │ ▼ ▼ ┌─────────────────────────────────────────┐ │ GM (Global Memory) │ └─────────────────────────────────────────┘ ▲ ▲ │ │ 没有一致性协议一个人写的东西另一个人不一定看得到CPU 上有 MESI 等缓存一致性协议硬件帮你通知对端刷缓存。910B 上没有 —— 这两条路互相不可见。2. 什么是标量访问 vs “MTE 搬运”维度标量访问MTE 搬运写法gmPtr[i] 1.0f;DataCopy(dst, src, len);通路走 DataCache走 DMA 引擎粒度单个元素float/int一整块连续内存适合场景少量、零散操作大块、批量搬运是否进 cache是DataCache否直通 DRAM简单说标量写 你往一个小信箱DataCache里塞纸条等攒够了一批才统一寄出去MTE 搬运 叫搬运工DMA一次性把一车货从仓库搬到工作台问题来了小信箱和搬运工之间没有对讲机。你塞进去的纸条搬运工不一定知道搬运工刚搬走的东西你的小信箱里可能还留着旧纸条。3. 生活中的比喻想象你和小李合租一个仓库你 标量通路写完只在自己的小本子DataCache上记一笔小李 MTE 搬运工只看仓库门牌DRAM从不翻你的小本子场景 A你先记小李后搬1. 你把还剩 5 个箱子写在自己的小本子上 ← DataCache 里有新值 2. 小李按门牌去仓库搬货 ← MTE 读 DRAM看到的是旧值 3 3. 你的小本子和仓库不一致了场景 B小李先搬你后记1. 小李刚把仓库里3 个箱子改成0 个 ← MTE 写 DRAM异步还在路上 2. 你在仓库门牌上写还剩 8 个 ← 标量写覆盖了 DRAM 3. 几秒后小李的 DMA 到了把你的8盖成0 ← 你的写丢了两个方向都会出错4. Bug 的两个具体方向方向 1标量写 → MTE 读// 标量写把 computed_value 写到 GM 缓冲区__gm__float*dSF32/* GM scratch */;for(uint32_ti0;iN;i){dSF32[i]computed_value;// 进了 DataCache不一定到 DRAM}// MTE 读把同一块 GM 搬到 UBDataCopy(ubBuf,dSF32,N);// DMA 直读 DRAM看不到 DataCache → 读到旧值方向 2MTE 写 → 标量写// MTE 写把工作台上的 zeroBuf 搬到 GM异步DataCopy(dWacc,zeroBuf,V*H);// DMA 还在路上// 标量写在同一地址上累加for(uint32_ti0;iV*H;i){dWacc[i]partial_sum;// 你的写可能被迟到的 DMA 盖掉}症状精度误差在1e-3 ~ 2e-2级别FP16 正常误差约 1e-4没有任何编译/运行报错只是结果不对。5. 简单复现代码host 端模拟下面这段独立可编译的 C 代码模拟了910B 的两条不互通通路在标准 CPU 上也能看到类似现象。它不是 AscendC 代码但用最少的代码把双向不一致这件事演示清楚// simulate_910b_incoherence.cpp// 编译g -stdc17 -O2 simulate_910b_incoherence.cpp -o sim ./sim//// 模拟 910B 上标量通路和DMA 通路共享同一块 GM// 但两边没有缓存一致性协议。#includecstdio#includecstring#includevector// 模拟标量通路的小本子DataCachestaticfloatg_scalar_notebook[16]{0};// 模拟GM 仓库DRAM刚开始是 0staticfloatg_gm[16]{0};// 模拟MTE 搬运工看到的 DRAM 视图staticfloatg_dma_view[16]{0};// 模拟标量通路把值写进小本子但不一定立刻同步到 GMvoidscalar_write(inti,floatv){g_scalar_notebook[i]v;// 910B 上这一步只是写 DataCacheDRAM 还没收到g_gm[i]v;// 模拟已同步到 DRAM —— 但实际硬件不保证}// 模拟 MTE 搬运工直接读 DRAM完全不知道小本子的存在voidmte_read_all(){memcpy(g_dma_view,g_gm,sizeof(g_gm));}// 模拟 MTE 写搬运工直接把一车零倒进 GMvoidmte_write_zeros(){// 标量通路可能不知道搬运工正在路上memset(g_gm,0,sizeof(g_gm));// 910B 上这是异步 DMA标量通路的小本子里仍是旧值g_scalar_notebook[0]42.0f;// 标量写把自己小本子改了// 如果 DMA 比这个标量写晚到标量写就被覆盖}intmain(){// 方向 1标量写 → MTE 读 printf( 方向 1标量写 - MTE 读 \n);for(inti0;i8;i)scalar_write(i,(float)(i1));// 假设标量通路忘了刷回 DataCacheMTE 只看到旧值// 我们手动把未同步状态模拟出来让 g_gm 保持为 0memset(g_gm,0,sizeof(g_gm));// 模拟 DRAM 实际还是旧值mte_read_all();printf(标量写的期望值: 1 2 3 4 5 6 7 8\n);printf(MTE 读到的实际: );for(inti0;i8;i)printf(%.0f ,g_dma_view[i]);printf( ← 全是旧值\n\n);// 方向 2MTE 写 → 标量写 printf( 方向 2MTE 写 - 标量写 \n);mte_write_zeros();// 搬运工把 GM 清零// 标量通路以为自己在 g_gm[0] 上写了 42但迟到的 DMA 可能盖掉// 我们模拟搬运工迟到把 g_gm[0] 改回 0g_gm[0]0.0f;// 模拟迟到的 DMA 写到达printf(标量写期望 g_gm[0] 42\n);printf(实际 g_gm[0] %.0f ← 被 DMA 覆盖了\n,g_gm[0]);return0;}运行结果标准 Linux 上即可复现这个两个方向都不一致的演示 方向 1标量写 - MTE 读 标量写的期望值: 1 2 3 4 5 6 7 8 MTE 读到的实际: 0 0 0 0 0 0 0 0 ← 全是旧值 方向 2MTE 写 - 标量写 标量写期望 g_gm[0] 42 实际 g_gm[0] 0 ← 被 DMA 覆盖了真实 910B 上是硬件帮你复制粘贴了这段故事DataCache 和 DMA 通路对同一地址的写入时序是不确定的谁最后到 DRAM 谁就赢。6. 真实 AscendC 代码长什么样❌ 错误写法触发 bug// kernel 内在 GM scratch 上做中间累加__gm__float*dSF32/* GM scratch */;// 方向 1标量写 GMfor(uint32_ti0;iN;i){dSF32[i]computed_value;// ← 写 DataCache}// 方向 1 后续MTE 读同一块 GMDataCopy(ubBuf,dSF32,N);// ← DMA 看不到 DataCache 的新值// —— 或者 ——// 方向 2MTE 写 GMDataCopy(dWacc,zeroBuf,V*H);// ← 异步 DMA// 方向 2 后续标量写同一地址for(uint32_ti0;iV*H;i){dWacc[i]partial_sum;// ← 可能被迟到的 DMA 覆盖}✅ 正确写法三种策略任选一种策略 1推荐在 UB 里完成所有中间计算根本不碰 GMTPipe ep;TBufTPosition::VECINeb;ep.InitBuffer(eb,ubSize);LocalTensorfloatubBufeb.Getfloat(N);// 全程在 UB 中计算for(uint32_ti0;iN;i){ubBuf.SetValue(i,computed_value);}// 最后一次性 DataCopy 到 GMDataCopy(gmOut,ubBuf,N);策略 2全程用标量访问不混 MTE// 清零标量写for(uint32_ti0;iV*H;i){dWacc[i]0.0f;}// 累加也是标量写同一通路 → 一致for(uint32_ti0;iV*H;i){dWacc[i]partial;}策略 3标量写后显式刷 DataCacheGlobalTensorDTgScratch;gScratch.SetGlobalBuffer((__gm__ DT*)scratch);// 标量写for(uint32_ti0;iN;i){gScratch.SetValue(i,(DT)computed_value);}// 显式刷回 DRAMDataCacheCleanAndInvalidDT,CacheLine::ENTIRE_DATA_CACHE(gScratch);// 现在 MTE 能读到一致的值DataCopy(ubBuf,gScratch,alignedN);7. 修复效果验证项修复前修复后改善Mode B grad_input 误差标量→MTE2.80e-31.53e-5183xBT edge tile grad_input 误差MTE→标量1.65e-22.44e-468xMode A 精度不受影响不受影响回归 OK误差从 1e-2 级别压到 1e-4~1e-5回到 FP16 的正常精度。8. 教训总结要点说明同一块 GM 只能走一种通路要么全程标量gmPtr[i]v要么全程DataCopyUB-only 中间计算是最优解既避免一致性陷阱又省 GM 带宽DataCacheCleanAndInvalid 是兜底实在要在 GM 上混用必须显式刷910B ≠ CPUCPU 有 MESI 自动帮你同步910B 没有症状很迷惑编译能过、运行不报错只是精度莫名变差 10~100 倍小 shape 更容易暴露BT4、V8 这种小规模反而最常触发附录什么时候应该怀疑这个 bug如果你看到以下任意一条先停下来检查代码里有没有 GM 上的标量/MTE 混用精度误差在1e-3 ~ 1e-2FP16 正常 ~1e-4同样的代码逻辑在 910A / 950 上没问题只在 910B 上飘消除 GM 中间缓冲后精度恢复正常gmPtr[i] v和DataCopy(..., gmPtr, ...)出现在同一地址没有编译错误、没有运行错误只是结果不对满足其中 2~3 条基本就是这个问题。改成 UB-only 中间计算立竿见影。