流水线与并行执行¶
本文档介绍 PTO 的流水线模型和并行执行机制,帮助开发者充分利用硬件资源实现高性能算子。
目录¶
1. 流水线概述¶
1.1 什么是流水线¶
流水线是一种并行技术,将任务分解为多个阶段,不同阶段可以同时处理不同的数据。
类比:汽车装配线 - 阶段1:安装底盘 - 阶段2:安装发动机 - 阶段3:安装车身 - 阶段4:喷漆
当阶段2在处理车辆B时,阶段1可以同时处理车辆C。
1.2 PTO 中的流水线¶
PTO 算子通常包含以下阶段:
TLOAD → Transform → Compute → TSTORE
↓ ↓ ↓ ↓
MTE2 MTE1 CUBE/VEC MTE1
关键思想:让不同阶段重叠执行,提高硬件利用率。
2. 硬件流水线¶
2.1 Ascend 硬件流水线¶
Ascend AI 处理器包含多个独立的执行单元:
| 流水线 | 功能 | 典型指令 |
|---|---|---|
| MTE2 | GM → L1 数据搬运 | TLOAD |
| MTE1 | L1 → L0 数据搬运 | TEXTRACT, TMOV |
| CUBE | 矩阵乘法 | TMATMUL |
| VECTOR | 逐元素运算 | TADD, TEXP, TMAX |
| SCALAR | 标量运算和控制流 | 地址计算、循环控制 |
2.2 流水线并行¶
不同流水线可以同时执行:
// 时间 T0: TLOAD 在 MTE2 上执行
TLOAD(tileA[0], ...);
// 时间 T1: TLOAD 继续,同时 TEXTRACT 在 MTE1 上执行
TLOAD(tileA[1], ...);
TEXTRACT(tileLeft[0], tileA[0]);
// 时间 T2: 三个流水线同时工作
TLOAD(tileA[2], ...);
TEXTRACT(tileLeft[1], tileA[1]);
TMATMUL(acc, tileLeft[0], tileRight[0]);
性能提升:理想情况下可以达到 3-4× 的吞吐量提升。
3. 软件流水线¶
3.1 双缓冲技术¶
双缓冲是最常用的软件流水线技术:
// 基础版本(无流水线)
for (int i = 0; i < N; i++) {
TLOAD(tile, ...); // 等待加载
TCOMPUTE(result, tile); // 等待计算
TSTORE(..., result); // 等待存储
}
// 总时间 = N × (T_load + T_compute + T_store)
// 双缓冲版本(有流水线)
TLOAD(tile[0], ...); // 预加载第一个
for (int i = 0; i < N; i++) {
int curr = i % 2;
int next = (i + 1) % 2;
// 当前迭代计算
TCOMPUTE(result[curr], tile[curr]);
// 同时加载下一个
if (i + 1 < N) {
TLOAD(tile[next], ...);
}
// 存储结果
TSTORE(..., result[curr]);
}
// 总时间 ≈ N × max(T_load, T_compute, T_store)
性能提升:当三个阶段时间相近时,可以达到接近 3× 的加速。
3.2 多级流水线¶
对于复杂算子,可以使用多级流水线:
// GEMM 的三级流水线
for (int k = 0; k < K; k += tileK) {
int curr = k % 2;
int next = (k + tileK) % 2;
// 阶段1: MTE2 加载下一批数据
if (k + tileK < K) {
TLOAD(tileA_L1[next], ...);
TLOAD(tileB_L1[next], ...);
}
// 阶段2: MTE1 提取当前数据到 L0
TEXTRACT(tileA_L0[curr], tileA_L1[curr]);
TEXTRACT(tileB_L0[curr], tileB_L1[curr]);
// 阶段3: CUBE 计算
TMATMUL(acc, tileA_L0[curr], tileB_L0[curr]);
}
3.3 事件同步¶
使用 Event 确保流水线的正确性:
Event<Op::TLOAD, Op::TEXTRACT> e_load;
Event<Op::TEXTRACT, Op::TMATMUL> e_extract;
Event<Op::TMATMUL, Op::TMOV> e_compute;
for (int k = 0; k < K; k += tileK) {
// 加载并记录事件
e_load = TLOAD(tileA, ...);
// 等待加载完成,然后提取
e_extract = TEXTRACT(tileLeft, tileA, e_load);
// 等待提取完成,然后计算
e_compute = TMATMUL(acc, tileLeft, tileRight, e_extract);
}
关键原则: - 只等待真实的数据依赖 - 避免不必要的全局同步 - 使用细粒度的 producer-consumer 事件
4. 并行执行模型¶
4.1 多核并行(Block 级)¶
PTO 支持多核并行执行,每个核处理不同的数据块:
__global__ __aicore__ void MatMulKernel(...) {
// 获取当前核的 ID
int block_idx = get_block_idx();
int block_m = block_idx / N_blocks;
int block_n = block_idx % N_blocks;
// 计算当前核负责的数据范围
int m_start = block_m * TILE_M;
int n_start = block_n * TILE_N;
// 处理当前块
for (int k = 0; k < K; k += TILE_K) {
TLOAD(tileA, A[m_start:m_start+TILE_M, k:k+TILE_K]);
TLOAD(tileB, B[k:k+TILE_K, n_start:n_start+TILE_N]);
TMATMUL(acc, tileA, tileB);
}
TSTORE(C[m_start:m_start+TILE_M, n_start:n_start+TILE_N], acc);
}
并行策略: - 2D 划分:同时切分 M 和 N 维度 - 负载均衡:确保每个核的工作量相近 - 数据局部性:减少核间通信
4.2 核内并行(Tile 级)¶
单个核内,Tile 操作本身是并行的:
// TADD 会并行处理 Tile 中的所有元素
TADD(c, a, b); // 16×16 = 256 个元素并行相加
硬件实现: - Vector 单元:SIMD 并行处理多个元素 - Cube 单元:矩阵乘法的并行计算
4.3 流水线并行(阶段级)¶
如前所述,不同流水线阶段可以并行执行。
三级并行:
Block 级并行(多核)
↓
Tile 级并行(SIMD)
↓
Pipeline 级并行(阶段重叠)
5. 性能优化技巧¶
5.1 识别瓶颈¶
使用 profiler 分析各阶段的时间占比:
msprof --application="your_app" --output=./profiling_data
瓶颈类型: - TLOAD 占主导:内存带宽受限 → 提升数据复用 - TMATMUL 占主导:计算受限 → 已接近理论峰值 - TEXTRACT 占主导:布局转换开销大 → 优化数据布局
5.2 优化流水线重叠¶
目标:让最慢的阶段决定总时间。
// 不好的例子:串行执行
for (int i = 0; i < N; i++) {
TLOAD(tile, ...); // 10ms
TCOMPUTE(result, tile); // 5ms
TSTORE(..., result); // 3ms
}
// 总时间 = N × 18ms
// 好的例子:流水线重叠
// 预加载 + 双缓冲
// 总时间 ≈ N × 10ms(由最慢的 TLOAD 决定)
5.3 调整 Tile 大小¶
权衡: - 大 Tile:更好的数据复用,但可能超出片上容量 - 小 Tile:更灵活,但开销占比增大
// 示例:GEMM 的 Tile 大小选择
// A2/A3: baseM=128, baseK=64, baseN=256
// A5: baseM=256, baseK=128, baseN=512(更大的片上容量)
5.4 减少同步开销¶
原则: - 只在必要时同步 - 使用细粒度事件而非全局屏障 - 在稳态循环中避免 drain
// 不好:每次迭代都全局同步
for (int i = 0; i < N; i++) {
TLOAD(tile, ...);
TSYNC<Op::TLOAD>(); // 全局同步,开销大
TCOMPUTE(result, tile);
}
// 好:使用事件表达依赖
Event<Op::TLOAD, Op::TADD> e;
for (int i = 0; i < N; i++) {
e = TLOAD(tile, ...);
TCOMPUTE(result, tile, e); // 只等待 TLOAD 完成
}
5.5 数据复用¶
策略: - 在 L1 中缓存频繁访问的数据 - 在 K 维度分块以复用 A 和 B
// GEMM 示例:K 维度分块
for (int k = 0; k < K; k += TILE_K) {
TLOAD(tileA, A[m:m+M, k:k+TILE_K]); // 加载一次
TLOAD(tileB, B[k:k+TILE_K, n:n+N]); // 加载一次
TMATMUL(acc, tileA, tileB); // 复用计算
}
// 每个元素被加载一次,但参与多次计算
6. 实战案例¶
6.1 GEMM 流水线优化¶
优化前:
for (int k = 0; k < K; k += tileK) {
TLOAD(tileA, ...);
TLOAD(tileB, ...);
TEXTRACT(tileLeft, tileA);
TEXTRACT(tileRight, tileB);
TMATMUL(acc, tileLeft, tileRight);
}
// TLOAD 占比 80%,TMATMUL 占比 15%
优化后(双缓冲 + 流水线):
// 预加载
TLOAD(tileA[0], ...);
TLOAD(tileB[0], ...);
for (int k = 0; k < K; k += tileK) {
int curr = k % 2;
int next = (k + tileK) % 2;
// 提取当前数据
TEXTRACT(tileLeft[curr], tileA[curr]);
TEXTRACT(tileRight[curr], tileB[curr]);
// 同时加载下一批
if (k + tileK < K) {
TLOAD(tileA[next], ...);
TLOAD(tileB[next], ...);
}
// 计算
TMATMUL(acc, tileLeft[curr], tileRight[curr]);
}
// TLOAD 占比 45%,TMATMUL 占比 55%
// 性能提升 3.2×
详细分析:GEMM 性能优化案例
6.2 Flash Attention 多阶段流水线¶
Flash Attention 包含多个计算阶段,需要精心设计流水线:
// 阶段1: 计算 QK^T
// 阶段2: Softmax
// 阶段3: 计算 PV
// 阶段4: 更新输出
// 使用多级流水线重叠这些阶段
详细实现:Flash Attention 优化
7. 调试技巧¶
7.1 验证流水线正确性¶
步骤: 1. 先实现串行版本,验证正确性 2. 逐步添加流水线优化 3. 每次优化后验证数值结果
#ifdef DEBUG
// 在关键点检查中间结果
float max_diff = CheckNumericalError(result, expected);
assert(max_diff < 1e-5);
#endif
7.2 性能分析¶
工具:
- msprof:硬件性能分析
- 手动计时:关键阶段的时间测量
// 手动计时示例
auto start = GetTime();
for (int i = 0; i < N; i++) {
TLOAD(tile, ...);
}
auto end = GetTime();
printf("TLOAD time: %f ms\n", (end - start) / N);
8. 最佳实践总结¶
✅ DO: - 使用双缓冲实现流水线重叠 - 用事件表达细粒度依赖 - 分析 profiler 数据识别瓶颈 - 调整 Tile 大小平衡复用和容量 - 在稳态循环中最大化重叠
❌ DON'T: - 过度同步(避免不必要的全局屏障) - 忽略数据复用机会 - Tile 过大导致溢出片上容量 - 在优化前不验证正确性