前沿计组知识入门(三)
并行计算与编程模型
1. 课程回顾与实验思考
- 回顾:上节课介绍了吞吐量计算硬件的三个核心思想:多核执行、单指令多数据(SIMD)执行和硬件多线程。
- 实验思考:通过向量元素逐点乘法任务引出问题,探讨这种计算是否适合现代吞吐量导向的并行处理器。
- 示例代码:
// 向量逐点乘法 for (int i = 0; i < N; i++) { C[i] = A[i] * B[i]; }
- 分析:这种计算任务包含大量独立的乘法操作,非常适合并行处理。现代GPU等并行处理器可以通过多线程或多核心同时执行这些操作,从而显著提高性能。
- 示例代码:
2. NVIDIA V100 GPU 架构
- V100 GPU 拥有80个流式多处理器(SM),每个SM有64个32位浮点ALU,总计5120个ALU。
- 配备6MB L2缓存和16GB HBM(高带宽存储器),带宽高达900GB/s。
- 问题引出:如何为大量ALU提供足够的数据?
- 分析:对于大规模并行计算,内存带宽成为关键瓶颈。即使有强大的计算能力,如果数据无法及时供应,ALU也会空闲,导致性能下降。
3. 延迟与带宽的理解
- 延迟:从旧金山开车到斯坦福需要0.5小时,表示完成一项任务的时间。
- 吞吐量:通过提高车速或增加车道数量来提高单位时间内完成的任务数量。
- 类比高速公路:
- 提高车速:将车速从100公里/小时提高到200公里/小时,吞吐量从2辆/小时提高到4辆/小时。
- 增加车道:保持车速不变,增加车道数量,吞吐量从2辆/小时提高到8辆/小时。
- 优化调度:通过更高效地利用车道(如减少车距),进一步提高吞吐量。
4. 内存带宽与延迟
- 内存带宽:内存系统向处理器提供数据的速率,例如20GB/s。
- 延迟:传输单个数据项所需的时间。
- 类比洗衣机和烘干机:
- 增加资源:通过多台洗衣机和烘干机同时工作,提高洗衣的吞吐量。
- 优化流程:通过流水线化(如洗完一批立即烘干)进一步提高效率。
5. 管线化与并行计算
- 类比管道:通过优化数据传输来提高系统的吞吐量。
- 应用到计算机程序:通过优化指令管线化来提高效率。
- 示例代码:
// 指令管线化示例 for (int i = 0; i < N; i++) { C[i] = A[i] * B[i]; // 计算 D[i] = C[i] + 1; // 后续操作 }
- 分析:通过将计算和后续操作分离,可以实现更高的吞吐量。每个阶段可以并行执行,从而减少整体执行时间。
- 示例代码:
6. 内存带宽限制
- 示例代码:
for (int i = 0; i < N; i++) { C[i] = A[i] * B[i]; }
- 分析:这个程序需要频繁访问内存,而内存带宽可能成为性能瓶颈。为了充分利用GPU等并行处理器,需要减少内存访问频率。
- 优化策略:
- 数据重用:尽量减少内存访问,重用已加载的数据。
- 减少存储操作:通过在寄存器中进行更多计算,减少对内存的读写操作。
7. ISPC(Intel SPMD Program Compiler)
- ISPC是一种单程序多数据(SPMD)编程模型,用于编写并行程序。
- 示例代码:
export void ispc_sinx(uniform int N, uniform int terms, uniform float* x, uniform float* result) { for (uniform int i = 0; i < N; i += programCount) { int idx = i + programIndex; float value = x[idx]; float numer = x[idx] * x[idx] * x[idx]; uniform int denom = 6; // 3! uniform int sign = -1; for (uniform int j = 1; j <= terms; j++) { value += sign * numer / denom; numer *= x[idx] * x[idx]; denom *= (2 * j + 2) * (2 * j + 3); sign *= -1; } result[idx] = value; } }
- 分析:通过
programCount
和programIndex
,ISPC可以将任务分配给多个执行实例,从而实现并行计算。
- 分析:通过
8. ISPC的抽象与实现
- ISPC通过
foreach
关键字提供了一种高级抽象,允许程序员以类似顺序编程的方式编写并行代码。 - 示例代码:
export void ispc_sinx_foreach(uniform int N, uniform int terms, uniform float* x, uniform float* result) { foreach (i = 0 ... N) { float value = x[i]; float numer = x[i] * x[i] * x[i]; uniform int denom = 6; // 3! uniform int sign = -1; for (uniform int j = 1; j <= terms; j++) { value += sign * numer / denom; numer *= x[i] * x[i]; denom *= (2 * j + 2) * (2 * j + 3); sign *= -1; } result[i] = value; } }
- 分析:
foreach
关键字简化了任务分配过程,程序员只需关注每个任务的逻辑,而由编译器处理并行执行的细节。
- 分析:
9. ISPC的跨实例操作
- ISPC提供了跨实例操作,例如
reduce_add
,用于在多个实例之间进行数据汇总。 - 示例代码:
export uniform float sum_array(uniform int N, uniform float* x) { float partial = 0.0f; foreach (i = 0 ... N) { partial += x[i]; } uniform float sum = reduce_add(partial); return sum; }
- 分析:
reduce_add
函数允许在多个实例之间汇总数据,最终返回一个统一的结果。这种操作在并行计算中非常常见,例如计算数组的总和。
- 分析:
10. 并行程序的创建
- 创建并行程序的过程包括:分解任务、分配任务、协调执行和映射到硬件。
- Amdahl定律:程序的加速比受到串行部分的限制。
- 公式:最大加速比 ≤ 1/S,其中S是程序中串行部分的比例。
- 示例代码:
// 串行部分 int sum = 0; for (int i = 0; i < N; i++) { sum += A[i]; }
- 分析:即使并行部分的加速比很高,串行部分仍然会限制整体的加速比。因此,减少串行部分的比例是提高并行程序性能的关键。
11. 并行程序的优化
- 负载均衡:确保所有处理器在程序执行期间都在计算。
- 静态分配:在程序运行前确定任务分配。
- 动态分配:在程序运行时动态分配任务。
- 示例代码:
// 动态分配任务 int counter = 0; while (true) { int i; lock(counter_lock); i = counter++; unlock(counter_lock); if (i >= N) break; is_prime[i] = test_primality(x[i]); }
- 分析:通过动态分配任务,可以更好地应对任务数量或任务执行时间不确定的情况。锁的使用确保了任务分配的正确性,但也可能引入同步开销。
12. Fork-Join 并行模式
- Fork-Join模式是一种自然表达分治算法的方式。
- 示例代码(Cilk Plus):
void quick_sort(int* begin, int* end) { if (begin >= end - PARALLEL_CUTOFF) { std::sort(begin, end); } else { int* middle = partition(begin, end); cilk_spawn quick_sort(begin, middle); quick_sort(middle + 1, end); } }
- 分析:
cilk_spawn
用于启动子任务,cilk_sync
用于等待所有子任务完成。这种模式特别适合分治算法,因为它可以自然地表达递归分解和并行执行。
- 分析:
13. Cilk Plus 的工作窃取调度
- Cilk Plus运行时通过工作窃取调度器实现
spawn
和sync
抽象。 - 工作队列:每个线程都有自己的工作队列,空闲线程可以从其他线程的队列中窃取工作。
- 示例代码:
for (int i = 0; i < N; i++) { cilk_spawn foo(i); } cilk_sync;
- 分析:工作窃取调度器通过动态分配任务,确保所有线程始终保持忙碌,从而提高并行效率。这种调度方式特别适合任务数量不确定或任务执行时间不均匀的情况。
14. 总结
- 并行计算的关键在于识别程序中的依赖关系,并通过合理的任务分解、分配和协调来提高性能。
- ISPC和Cilk Plus等工具提供了强大的抽象和调度机制,帮助程序员编写高效的并行程序。
- 优化并行程序时,需要综合考虑负载均衡、任务分配策略以及硬件资源的利用效率,以实现最佳性能。