第一章:原子操作总览

1.1 为什么需要原子操作

问题场景: 多个 Scalar 核同时修改同一 GM 地址

Core 0: LD X0, [counter]    → X0 = 100
Core 1: LD X0, [counter]    → X0 = 100  (都读到了 100)
Core 0: ADD X0, X0, #1      → X0 = 101
Core 1: ADD X0, X0, #1      → X0 = 101  (各自加 1)
Core 0: ST X0, [counter]    → [counter] = 101
Core 1: ST X0, [counter]    → [counter] = 101  (应该是 102!)

两次 +1, 但结果只 +1 了 → 数据竞争 (Race Condition)

原子操作解决:
Core 0: ATOM.add.u32 Xm, [counter], Xt  → [counter] = 101 (原子完成)
Core 1: ATOM.add.u32 Xm, [counter], Xt  → [counter] = 102 (原子完成)

1.2 四类原子指令定位

┌─────────────────────────────────────────────────────────┐
│              Scalar 核原子操作分类                         │
├──────────────┬──────────┬───────────┬───────────────────┤
│ 指令         │ 总线     │ 地址空间   │ 返回旧值?         │
├──────────────┼──────────┼───────────┼───────────────────┤
│ ATOM         │ 系统总线  │ GM (OUT)  │  返回旧值        │
│ RED          │ 系统总线  │ GM (OUT)  │ 不返回            │
└──────────────┴──────────┴───────────┴───────────────────┘

第二章:ATOM — 全局内存原子操作

2.1 指令格式

T AtomicCAS(__gm__ T *addr, T compare, T val);
T AtomicExch(__gm__ T *addr, T val);
T AtomicSub(__gm__ T *addr, T val);
T AtomicAdd(__gm__ T *addr, T val);
T AtomicMin(__gm__ T *addr, T val);
T AtomicMax(__gm__ T *addr, T val);

参数说明:
  T — 数据类型: u32/s32/u64/s64

2.2 各操作语义详解

.cas — Compare-And-Swap (核心同步原语)

// C 语言等价语义
uint32_t AtomicCAS(uint32_t *addr, uint32_t expected, uint32_t desired) {
    uint32_t old = *addr;
    if (old == expected) {
        *addr = desired;    // 匹配则写入 desired
    }
    return old;             // 总是返回旧值
}

.exch — 原子交换

// C 语言等价语义
uint32_t AtomicExch(uint32_t *addr, uint32_t new_val) {
    uint32_t old = *addr;
    *addr = new_val;
    return old;
}

.add — 原子加

// C 语言等价语义
uint32_t AtomicAdd(uint32_t *addr, uint32_t val) {
    uint32_t old = *addr;
    *addr = old + val;
    return old;
}

.min / .max — 原子最小/最大值

// C 语言等价语义
uint32_t AtomicMax(uint32_t *addr, uint32_t val) {
    uint32_t old = *addr;
    *addr = (val > old) ? val : old;
    return old;
}

第三章:RED — 全局内存归约操作

3.1 指令格式

void AtomicAdd(__gm__ T *addr, T val);
void AtomicMin(__gm__ T *addr, T val);
void AtomicMax(__gm__ T *addr, T val);


参数说明:
  .T — 数据类型: u32/s32/f16/bf16/f32

关键区别:
  - 不返回旧值
  - 支持浮点类型 (f16/bf16/f32) ← ATOM 不支持!

3.3 语义

// *addr = *addr op Xm  (直接在目标地址上归约, 不返回旧值)
*Xn = *Xn op Xm;

// 例: RED.add.f32 [Xn], Xm
// *Xn = *Xn + Xm  (原子浮点累加, 不返回旧值)

3.4 ATOM vs RED 选择指南

┌─────────────────────────────────────────────────┐
│            ATOM vs RED 选择决策                   │
├─────────────────────────────────────────────────┤
│                                                 │
│  需要读取旧值?                                    │
│    ├── YES → ATOM                               │
│    │    适用: 锁实现, 需要判断是否成功的场景       │
│    │                                             │
│    └── NO → 数据类型是浮点?                      │
│         ├── YES → RED (唯一能做浮点原子归约的)    │
│         │    适用: AllReduce float, 全局浮点累加  │
│         │                                        │
│         └── NO (整数)                            │
│              ├── 需要知道旧值 → ATOM             │
│              └── 纯归约 → RED (省掉 Xt 寄存器)   │
└─────────────────────────────────────────────────┘

第四章:实战编程模式

4.1 Spin Lock (自旋锁)

// ══════════════════════════════════════════════════
// 自旋锁: 用 CAS 实现
// lock_addr 指向一个 u32 变量: 0=解锁, 1=加锁
// ══════════════════════════════════════════════════

// 获取锁
  // expected = 0 (未锁),  desired  = 1 (加锁)
  while( true){  
      if( AtomicCAS(gm, 0, 1) == 0  ) { //返回0, 表示加锁成功
         break;
      }
  }
//获取到锁,处理业务。

//释放锁
  AtomicSub(gm, 1);   // 写 0 释放锁
  DSB(ALL)             // 确保后续指令可见,  按需写,不是必须的
  // 清 dcache,  这个是不需要的, 因为atomic 是 bypass DCache

4.2 多核 Barrier (栅栏同步)

// ══════════════════════════════════════════════════
// Barrier: N 个 Core 都到达后才能继续
// barrier_counter: 初始为 0, 每个到达的 Core 原子 +1
// ══════════════════════════════════════════════════

// 每个 Core 到达 barrier 时:
  auto barrier_counter = AtomicAdd(gm, 1);  //  barrier_counter 旧值 (= 之前已到达的 Core 数)
  if( barrier_counter == N ) {
    sfv(); //唤醒
  }
  else {
    while( barrier_counter != N ){
      wfe();  //休眠    
      barrier_counter = AtomicAdd(gm, 0);
    }
  }

第五章:Cache 一致性维护 (核心关键)

5.1 原子操作的 Cache 行为

┌─────────────────────────────────────────────────────────┐
│           ATOM/RED 的 Cache 行为                         │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ATOM/RED 旁路 DCache  :                                │
│                                                         │
│    CPU Core                                             │
│      │                                                  │
│      ├── LD / ST ──→ DCache ──→ L2 ──→ GM               │
│      │                 ↑ 可能缓存旧值                    │
│      │                                                  │
│      └── ATOM/RED ────────────L2 ──────→ GM (直接!)     │
│                   旁路DCache, L2可配                    │
│                                                         │
│  问题:                                                  │
│    T0: LD X0, [addr]    → X0 = 100 (cache 中缓存 100)  │
│    T1: ATOM.add [addr]  → GM 中 addr 变成 101           │
│    T2: LD X0, [addr]    → X0 = 100 !! (cache hit 旧值!)│
│                                                         │
│  原子操作不会自动清理 dcache!                             │
└─────────────────────────────────────────────────────────┘

5.2 一致性维护协议

规则 1: ATOM/RED 之后, 如果需要通过 LD 读取同一地址, 必须清 cache
 
规则 2: 通过 LD 读取后, 如果之后要 ATOM 同一地址, 也建议清 cache
 
规则 3: 多核场景, 写端 ATOM 后通知读端, 读端必须先清 cache

5.3 饱和模式与 FP16 原子

当 CTRL[8:6] = 3'b010 (f16 原子) 时:

CTRL[48] = 0 (饱和模式):
  FP16 原子操作中:
    溢出 → 钳位到 ±65504
    NaN  → 钳位为 0
  适合推理场景

CTRL[48] = 1 (IEEE 模式):
  FP16 原子操作中:
    溢出 → ±INF
    NaN  → 正常传播
  适合训练场景

⚠ 警告: 修改 CTRL[48] 前, 如果之前执行过 FP16 的隐式原子存储,
  必须先执行 DCCI Xn, 1, #ATOMIC 清理残留 cache!

第六章:注意事项总结

6.1 硬约束 (违反会异常或数据错误)

编号 约束 后果 正确做法
A1 ATOM/RED 禁止访问栈地址 异常 只对 GM (OUT) 地址使用
A2 地址 bit[63:49] ≠ 0 地址溢出异常 确保地址在合法范围内
A3 地址未对齐到 type 大小 对齐异常 u32→4B 对齐, u64→8B 对齐
A4 cacheable 与 non-cacheable 地址相隔 < 4KB 数据不一致 地址规划预留 4KB 间隔
A5 ATOM/RED 计入 DSB.ALL 和 DSB.DDR 需等待完成 DSB 后才能确认操作完成

6.2 一致性约束 (违反会数据错误)

编号 约束 后果 正确做法
C1 ATOM/RED 后不清理 cache LD 读到旧值 ATOM → DSB → DCCI ATOMIC
C2 多核 ATOM 后不通知 对端不知道有新数据 ATOM → DSB → DCCI → SET_CROSS_CORE
C3 读取端不清理 cache 读到本地缓存的旧值 WAIT_FLAG → DSB → DCCI → LD
C4 切换 CTRL[48] 前不清理 ATOMIC cache FP16 饱和/IEEE 模式切换不一致 先 DCCI ATOMIC 再改 CTRL[48]
C5 SSBUF 上使用原子操作 SSBUF 不支持原子 只对 GM/HSCB 使用原子操作

6.3 性能注意事项

编号 注意事项 影响 建议
P1 ATOM 走系统总线, 延迟高 (~200ns) 频繁 ATOM 会成为瓶颈 减少原子操作频率, 批量累加后一次 ATOM
P2 ATOM.cas 自旋等待消耗总线带宽 锁竞争激烈时性能下降 临界区尽量短; 考虑公平锁
P3 每次都 DSB + DCCI 开销大 增加 10-20 周期 批量原子操作后统一 DSB + DCCI
P5 RED 比 ATOM 少返回旧值 少一个寄存器操作 不需要旧值时优先用 RED
P4 ATOM.u64 比 ATOM.u32 慢 64bit 原子操作带宽翻倍 能用 u32 就不用 u64

6.4 操作速查矩阵

需求 指令 数据类型 是否返回旧值 总线
原子加 (整数) ATOM.add u32/s32/u64/s64 Yes 系统总线
原子加 (浮点) RED.add f16/bf16/f32 No 系统总线
原子最大 (整数) ATOM.max u32/s32/u64/s64 Yes 系统总线
原子最大 (浮点) RED.max f16/bf16/f32 No 系统总线
原子最小 (整数) ATOM.min u32/s32/u64/s64 Yes 系统总线
原子最小 (浮点) RED.min f16/bf16/f32 No 系统总线
互斥锁 ATOM.cas u32/u64 Yes 系统总线
原子交换 ATOM.exch u32/u64 Yes 系统总线
Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐