【昇腾/AscendC开发】AscendC DataCopyPad 写出溢出 Bug 详解
AscendC DataCopyPad 写出溢出 Bug 详解
一句话总结
AscendC 的
DataCopyPad从片上缓存(UB)写数据到显存(GM)时,搬运单位(burst)必须 32 字节对齐。
当实际数据不够 32 字节的倍数时,DMA 引擎会多写几个字节(padding),覆盖掉相邻段的数据。
1. 背景知识:AscendC 的内存层次
┌──────────────────────────────────────────────┐
│ GM (Global Memory / 显存) │ ← 大容量(几十 GB),高延迟
│ │
│ 存放输入/输出张量:feat, offsets, out 等 │
└────────────────────┬─────────────────────────┘
│ DMA 搬运(DataCopyPad)
▼
┌──────────────────────────────────────────────┐
│ UB (Unified Buffer / 片上缓存) │ ← 小容量(~192KB),低延迟
│ │
│ 存放当前正在计算的一小块数据(累加器等) │
└──────────────────────────────────────────────┘
类比:
-
GM = 仓库(大但远,拿东西慢)
-
UB = 工作台(小但近,拿东西快)
-
DMA = 搬运工(负责仓库和工作台之间搬东西)
2. Segment Reduce 算子做什么?
把多行特征按"段"累加:
输入 feat(8 行 × 4 列): 输出 out(4 段 × 4 列):
row 0: [ 1, 2, 3, 4] ─┐
row 1: [ 5, 6, 7, 8] ─┘ seg 0 → [ 6, 8, 10, 12] (row0 + row1)
row 2: [ 9, 10, 11, 12] ─┐
row 3: [13, 14, 15, 16] │ seg 1 → [39, 42, 45, 48] (row2+3+4)
row 4: [17, 18, 19, 20] ─┘
seg 2 → [ 0, 0, 0, 0] (空段,无行)
row 5: [21, 22, 23, 24] ─┐
row 6: [25, 26, 27, 28] │ seg 3 → [75, 78, 81, 84] (row5+6+7)
row 7: [29, 30, 31, 32] ─┘
每个段的结果写回到 GM 中 out 数组对应的位置。
3. Bug 触发场景:feat_dim = 4
当 feat_dim = 4(每行 4 个 float)时:
一行数据大小 = 4 × 4 bytes = 16 bytes
DMA burst 对齐要求 = 32 bytes(最小搬运单位)
所以 DMA 实际搬运 32 字节,但有效数据只有 16 字节。
3.1 正常情况:写出 seg 0 的结果
out[] 在 GM 中的布局(每格 = 4 bytes / 1 个 float):
seg 0 (16B) seg 1 (16B) seg 2 (16B) seg 3 (16B)
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ 6 8 10 12│ │ ? ? ? ?│ │ ? ? ? ?│ │ ? ? ? ?│
└───────────┘ └───────────┘ └───────────┘ └───────────┘
↑ ↑
地址 0 地址 48
DMA 写 seg 0 结果 [6, 8, 10, 12]:
起始地址 = out + 0
burst 长度 = 32 字节(包含 16 字节数据 + 16 字节 padding)
写入后:
┌───────────┬───────────┐ ┌───────────┐ ┌───────────┐
│ 6 8 10 12│ PADDING!! │ │ ? ? ? ?│ │ ? ? ? ?│
└───────────┴───────────┘ └───────────┘ └───────────┘
↑
溢出到 seg 1 的位置!
seg 0 的 padding 覆盖了 seg 1 的前 4 个字节!
3.2 但是 seg 1 有自己的数据会覆盖回来
seg 1 有 3 行数据需要累加,计算完后写出:
DMA 写 seg 1 结果 [39, 42, 45, 48]:
起始地址 = out + 16
burst 长度 = 32 字节
写入后:
┌───────────┬───────────┬───────────┐ ┌───────────┐
│ 6 8 10 12│ 39 42 45 48│ PADDING!! │ │ ? ? ? ?│
└───────────┴───────────┴───────────┘ └───────────┘
↑
溢出到 seg 2 的位置!
3.3 关键!seg 2 是空段,不写出
seg 2 是空段(offsets[2] == offsets[3],没有行需要累加)
kernel 代码:if (count > 0) { DataCopyPad(...) } // count=0,跳过!
所以 seg 2 的内存不会被 kernel 写入,它的值保持的是 seg 1 写出时溢出的 padding 垃圾值!
最终 out[] 的内容:
┌───────────┬───────────┬───────────┬───────────┐
│ 6 8 10 12│ 39 42 45 48│ 0.1 -0.1...│ 75 78 81 84│
└───────────┴───────────┴───────────┴───────────┘
✅ seg 0 ✅ seg 1 ❌ seg 2 ✅ seg 3
正确 正确 被污染! 正确
这就是为什么 seg 2 应该是全 0,但实际读回来是 0.1, 0.1, 0.0, -0.1 等垃圾值。
4. 用更形象的比喻
想象你在白纸上写字,每行只能写 4 个数字(= feat_dim=4),但你用的笔每划必须写 8 个数字(= 32B burst 对齐):
纸上的格子:
第 1 行: [ ][ ][ ][ ] | [ ][ ][ ][ ] ← 第 2 行的位置
第 2 行: [ ][ ][ ][ ] | [ ][ ][ ][ ] ← 第 3 行的位置
第 3 行: [ ][ ][ ][ ] | [ ][ ][ ][ ] ← 第 4 行的位置
第 4 行: [ ][ ][ ][ ] |
你在第 1 行写 seg 0 的结果 [6, 8, 10, 12],
但笔画太粗,会多写出 [垃圾, 垃圾, 垃圾, 垃圾] 溢到第 2 行!
第 1 行: [ 6][ 8][10][12] | [垃][圾][垃][圾] ← 溢出了!
第 2 行: [39][42][45][48] | [垃][圾][垃][圾] ← 也溢出了!
第 3 行: [垃][圾][垃][圾] | ← 没人写,残留上一个人的垃圾
第 4 行: [75][78][81][84] | ← 写了自己的数据
第 3 行(seg 2)是空段,没人去写它 → 它的值就是第 2 行溢出的垃圾。
5. 为什么 feat_dim=8 不会触发?
feat_dim = 8 时:
一行数据大小 = 8 × 4 = 32 bytes
burst 大小 = 32 bytes
→ burst 大小 == 数据大小,没有 padding,不会溢出!
seg 0 (32B) seg 1 (32B) seg 2 (32B)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 6 8 10 12 14 16..│ │ 39 42 45 48 51..│ │ 0 0 0 0 0 ..│
└─────────────────┘ └─────────────────┘ └─────────────────┘
burst = 32B = 数据大小 → 完美对齐,无溢出 ✅
6. 修复方案
方案 A:约束 feat_dim 是 8 的倍数(我们采用的)
// segment_reduce_impl.cc 中的 fast path 条件
if (ascendc::HasAscendC() &&
op == "sum" &&
feat_dim % 8 == 0 && // ← 新增:保证 burst 对齐
feat_dim <= 16384) {
// 走 AscendC 快速路径
}
// 否则 fallback 到 host-mediated(D2H → CPU → H2D)
优点:简单、可靠,GNN 常用 feat_dim(16, 32, 64, 128, 256)都是 8 的倍数。
缺点:feat_dim=4 等小维度无法走快速路径(但走 fallback 仍然正确)。
方案 B:Host 端分配带 stride 的输出 buffer
让每段之间留 padding,这样溢出不会影响相邻段:
seg 0 (16B data) (16B gap) seg 1 (16B data) (16B gap)
┌───────────┬──────────┐ ┌───────────┬──────────┐
│ 6 8 10 12│ padding │ │ 39 42 45 48│ padding │
└───────────┴──────────┘ └───────────┴──────────┘
↑ 不影响下一段
优点:支持任意 feat_dim。
缺点:host 端需要额外的内存布局转换,复杂度高。
方案 C:用多次小 burst 写出
把一次 32B burst 拆成两次 16B —— 但 AscendC 的 burst 最小就是 32B,不可行。
7. SpMM 也有同样的问题吗?
是的! SpMM kernel(spmm_kernel.asc)的写出代码完全一样:
// spmm_kernel.asc 第 322-323 行
DataCopyPad(outGm, accumLocal,
{1, static_cast<uint16_t>(featBurstBytes_), 0, 0});
但 SpMM 的测试用例中 feat_dim 都是 8 的倍数(8, 16, 48, 64, 128),
所以没有触发这个 bug。我们已经在代码注释中标注了这个潜在风险。
8. 教训总结
| 要点 | 说明 |
|---|---|
| AscendC DMA burst 最小 32B | DataCopyPad 的 lenBurst 参数必须是 32 的倍数 |
| UB→GM 方向无 padding 控制 | 不像 GM→UB 有 DataCopyPadParams,UB→GM 只有 3 参数版本 |
| 溢出只影响空段 | 有数据的段会被后续写出覆盖回来,但空段不会被写 → 残留垃圾 |
| feat_dim 最好是 8 的倍数 | 8 × 4B = 32B,完美对齐,从根本上避免溢出 |
| 先测小维度 | 如果我们只测 feat_dim=128,永远不会发现这个 bug |
附录:实际调试日志
=== 修复前(feat_dim=4)===
Expected:
seg 0: 6.0 8.0 10.0 12.0
seg 1: 39.0 42.0 45.0 48.0
seg 2: 0.0 0.0 0.0 0.0 ← 期望全 0
seg 3: 75.0 78.0 81.0 84.0
Actual:
seg 0: 6.0 8.0 10.0 12.0 ✅
seg 1: 39.0 42.0 45.0 48.0 ✅
seg 2: 0.1 0.1 0.0 -0.1 ❌ ← 空段被 padding 污染
seg 3: 75.0 78.0 81.0 84.0 ✅
max_diff = 0.097168
=== 修复后(feat_dim=8)===
Results: 9/9 passed
ALL TESTS PASSED!
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐



所有评论(0)