---
tags: [CANN]
doc_type: knowledge-note
返回目录: "[[计算机系/昇腾与CANN/大模型与算子专题学习导航|大模型与算子专题学习导航]]"
上一篇: "[[计算机系/昇腾与CANN/Ascend C SIMD硬件内存与同步|Ascend C SIMD硬件内存与同步]]"
下一篇: "[[计算机系/昇腾与CANN/Ascend C Memory矢量算子开发|Ascend C Memory矢量算子开发]]"
---

# 流水线双向同步原理

![流水线双向同步](.assets/大模型与算子专题/流水线双向同步清晰图.svg)

## 1. 图中的 A 和 B 是什么

A、B 表示两条可异步执行的硬件流水线。例如 A 是负责 GM→UB 搬入的 MTE2，B 是使用 UB 数据进行向量计算的 Vector 流水线。

Scalar 按顺序发出命令，不意味着不同硬件流水线已经按该顺序完成。同步需要建立的是**完成与使用之间的依赖**。

这里假设 A、B 循环复用同一块缓冲：A 写入一块数据，B 读取并计算，然后 A 才能写入下一块。

## 2. 两个方向分别保护什么

| 方向 | 通知含义 | 等待者 | 避免的问题 |
| --- | --- | --- | --- |
| A→B，ready | A 的数据已经准备好 | B | B 读取尚未写完的数据 |
| B→A，free | B 已经用完当前缓冲 | A | A 的下一轮写入覆盖 B 仍在读取的数据 |

`notify(A, B, ready)` 表示 A 完成相关工作后向 B 发出完成信号；`wait(A, B, ready)` 表示 B 在收到该信号前不能继续执行依赖它的工作。

因此，`wait(A, B, ...)` 应画在 **B 的时间线上**。原图把这一等待画在 A 一侧，容易造成误解。清晰图按实际等待者重新安排了位置。

通知是同步标记，不负责搬运数据。它必须与正确的流水线和完成条件关联，而不是 Host 刚提交搬运就认为数据已经就绪。

## 3. 首轮、中间轮与末轮

首轮缓冲尚未被 B 使用，A 可以直接写入，不需要等待不存在的上一轮 free。A 写完后发 ready，B 等待后计算。

中间轮 A 必须等待上一轮的 free，确认 B 已读完，才能覆盖缓冲。随后仍执行 ready 通知、B 等待和消费、free 通知。

末轮 B 完成之后不存在下一次覆盖，可以省略 free 通知。**这不表示整个算子或设备已经完成**：最终输出、其他流水线和 Host 读取结果仍需要各自正确的同步。

只有一轮时，同时满足首轮和末轮条件：跳过起始 free 等待，执行 A、ready、B，跳过末尾 free 通知。

## 4. 机制伪代码

用 Notify/Wait 表达有限的 N 轮处理：

```cpp
// 机制伪代码：notify / wait 不代表可直接调用的 API 名称。
for (int i = 0; i < N; ++i) {
    if (i > 0) {
        wait(B, A, id);       // A 等待 B 用完上一轮数据
    }

    perform_pipelineA_tasks();
    notify(A, B, id);         // A 完成相关任务后通知 B

    wait(A, B, id);           // B 等待 A 的数据就绪
    perform_pipelineB_tasks();

    if (i < N - 1) {
        notify(B, A, id);     // B 完成相关任务后通知下一轮的 A
    }
}
```

这里参数按“信号来源流水线、信号接收流水线、事件编号”理解。`id` 用于配对通知与等待，不是核编号 `block_idx`。配对时要同时看方向和编号，不能只看数字相同。

`for (;;)` 本身表示无限循环，与 `not_last_time` 搭配时还需要额外的终止逻辑。上面的有限循环直接表达首轮与末轮条件，避免遗漏结束条件。

同一段控制代码可以向不同硬件流水线发射指令。代码先后排列不等于相关硬件任务已经完成，`wait` 的作用是约束接收流水线后续的依赖操作，不应理解成每一次都让 Host 或整个设备停住。

对应的数据依赖也可以写成：

```text
for i = 0 ... N-1:
    if i > 0:
        A 等待 B 的 free 信号

    A 执行第 i 块的生产任务
    A 完成后通知 B：ready

    B 等待 A 的 ready 信号
    B 执行第 i 块的消费任务

    if i < N-1:
        B 完成后通知 A：free
```

`ready`、`free` 是两类逻辑依赖，不规定一定需要两个不同数字 ID。实际事件槽、方向、编号和复用规则由所用 API 决定。

### A2/A3 与 Set/WaitFlag

A2/A3 的这里所述核内跨流水线同步使用 Notify/Wait 机制，对应 Set/WaitFlag 类接口：Set 发出完成标志，Wait 等待对应标志。950 新增的锁式机制不能直接套用到 A2/A3。

例如 A 为 `PIPE_MTE2`、B 为 `PIPE_V`，两条依赖分别是 MTE2→V 的“输入已搬完”和 V→MTE2 的“原缓冲已用完”。具体函数签名、事件类型和编号规则随 C API、C++ 基础 API 等接口层不同，需要使用同一套接口配对。

“A2/A3 只支持 Notify/Wait”是在比较这里的两种跨流水线机制，不表示 A2/A3 没有其他范围的同步功能，例如流水线屏障、核间同步或 Host 侧 Stream 等待。

## 5. 为什么同步过多也会降低性能

同一块缓冲必须遵守这些依赖，因此单缓冲示意中的 A 和 B 交替推进。这张图主要解释正确性，并不展示充分的搬运计算重叠。

双缓冲可以让 A 写另一块缓冲的下一批数据，同时 B 处理当前缓冲。但每一块缓冲仍需独立遵守“写完才能读，读完才能覆盖”。不能为了并行直接删除同步。

缺少 ready 等待可能读到旧数据或半写入数据；缺少 free 等待可能发生覆盖；等待没有配对通知可能死锁；将同步扩大到所有流水线可能损失不必要的并行度。

相关：[Ascend C SIMD 硬件内存与同步](Ascend%20C%20SIMD硬件内存与同步.md)。
