跳转至

Locality, Communication, and Contention

Parallel Programming: Case Study

我们通过一个例子作为引入, 让大家感受一下三种模型对应的并行方式的区别:

  1. data parallel: reduce ...
  2. shared address space (SPMD): lock() / unlock()
  3. message passing: send() / recv()

alt text

Data-parallel expression of solver

  • 分解 (Decomposition): 将单个网格元素的处理视为独立工作
  • 任务分配 (Assignment): 由系统处理
  • 协调通信 (Orchestration Communication): 由系统处理,通过内置的通信原语
  • 协调同步 (Orchestration: Synchronization): 由系统处理,for_all 块的结束隐式地等待所有工作线程完成

alt text

Shared address space (with SPMD threads) expression of solver

alt text

给个例子: 通过副本, 实现 barrier() 需求量减少 (3 -> 1)

alt text

Message passing expression of solver

alt text

alt text

我们深入讨论一下 send() && recv() 的形式:

[1] Synchronous (Blocking) Send and Receive

alt text

这样的话, 我们就发现上述 "msg passing solver" 的代码存在一个重大缺陷:

在上面的实现中,每个线程都遵循“先发送所有消息,再接收所有消息”的模式

假设我们有多个线程,例如线程 P0、P1、P2 等

  • P1 会尝试向 P0 send 一行数据,同时 P1 还会尝试向 P2 send 另一行数据
  • 与此同时,P0 也在尝试向 P1 send 数据,P2 也在尝试向 P1 send 数据

如果所有线程都同时执行它们的 send 操作:

  1. P1 调用 send 向 P0 发送。根据同步发送的定义,P1 会阻塞,等待 P0 调用 recv
  2. 同时,P0 也调用 send 向 P1 发送。P0 也会阻塞,等待 P1 调用 recv

结果是,P0 和 P1 都在等待对方执行 recv 操作,而它们自己都在 send 调用处阻塞,无法进行到 recv 操作。这种相互等待的局面会导致所有参与通信的线程都永久阻塞,从而形成死锁

这个问题本质的根源是: 消息发送和接收顺序不匹配

所有线程都试图首先发送消息,这意味着没有任何线程能够先执行接收操作来“解锁”其他线程的发送,从而导致所有线程相互等待,陷入死锁状态

[2] Synchronous (Blocking) Send and Receive 的暴力修正

alt text

[3] Non-blocking asynchronous send/recv

这才是上述deadlock最优雅的解决方案

  • send(): 立即返回, 返回一个handle || other work 可以继续干
  • recv(): 立即返回, 返回一个handle || other work 可以继续干
  • checksend(): 通过handle检查执行状态
  • checkrecv(): 通过handle检查执行状态

alt text

概念普及: 计算强度 (AI)

计算强度 (Arithmetic Intensity, AI)

\(AI = \frac{amount of computation (instructions)}{amount of communication (bytes)}\)

这个概念很直观, 肯定是越高越好(说明计算利用率更高)

Communication

Inherent vs. Artifactual Communication

(1) Inherent Communication:

  1. def: 由于并行需要, 必须要执行的 communication
  2. 可以通过不同的 assignment 策略来优化它的 AI (计算强度)

alt text

(2) Artifactual Communication:

  1. def: 除了inherent外的其他communication, 主要体现是: 不是算法逻辑本身所必需的,而是由系统实现的实际细节所导致的
  2. namely: 即使理论上可以避免的通信, 但由于硬件或软件工作方式的限制, 仍然会发生

简单理解成“额外开销”即可

形象理解
  • 固有通信: 你在完成一项任务(做饭)时,必须采购的食材(米、菜、肉),没有它们你无法完成任务
  • 人工通信: 你在采购过程中,因为超市的购物车太大、或者你记性不好,导致的一些额外支出或重复劳动

Techniques for Reducing Communication

  1. 从 cache hit 角度分析 (空间局部性):
    • "Blocking": reorder computation to make working sets map well to system’s memory hierarchy
  2. 从 程序利用率[循环融合] 角度分析 (时间局部性):
    • 在非融合版本中,每次单个数学操作(如 A[i] + B[i])涉及2次加载和1次存储(对于 tmp1)。因此, 算术强度为 1/3
    • 在融合版本中,对于数组中的每个 i,程序执行了3次数学操作 (A[i]+B[i], (A[i]+B[i])*C[i], (tmp)*D[i]),但只需要4次加载 (A[i], B[i], C[i], D[i]) 和1次存储 (E[i])。因此, 算术强度提高到了 3/5
  3. sharing data:
    • Schedule threads working on the "same data structure" at the same time on the "same processor"

Contention

contention (争夺)

(1) 引入: 由于共享资源的稀缺性, 越到后面等待的时间越长

alt text

(2) 何时发生: "多对一"

Contention occurs when many requests to a resource are made within a small window of time (the resource is a "hot spot")

我们可以考虑两种访问“共享资源”的方式:

  1. 扁平化访问 (flat comm):
    • Pros: 延迟很低
    • Cons: 很容易发生 contention
  2. 树状访问 (tree-struct comm):
    • Pros: 不容易发生 contention
    • Cons: 延迟较高

alt text

(3) 回顾: 上节课的 distributed work queues

本质是防止 "hot spot" 现象

  1. distributed queue: 每个thread先从自己的queue里取, 防止“一窝蜂取一个中心化的queue”
  2. random choice: 每个thread在自己queue清空后, 会 steal, 这是随机化的, 防止 "所有人同时save同一个人"

Performance Measurement: Roofline model

alt text

指标:

  • 横轴: Operation Intensity
    • 即: "运算强度", aka. Arithmetic Intensity (AI)
  • 纵轴: Attainable GFlops
    • 可达到的GFlops. 表示应用程序在给定机器上实际能够达到的 浮点运算性能
    • 直接衡量了程序的执行速度或吞吐量, 表示 "性能"

曲线:

  • 对角线区域: 内存带宽受限
    • 当AI较低时, 处理器需要频繁地从内存中获取数据, 但 每次获取的数据 所能支撑的计算量很少
    • 在这种情况下,即使 processor 的计算单元(ALU/FPU)非常快,它也会因为等待数据而空闲
  • 水平线区域: 计算资源受限
    • 当应用程序的运算强度足够高时: 意味着处理器每次从内存获取数据后,都能执行大量的计算。此时,数据供应已经不是瓶颈,处理器可以饱和地利用其计算单元

优化措施:

  • 对角线区域: 内存带宽受限
    • 需要提高 AI
    • 比如, 改进数据局部性, 减少 artificial comm, "一次加载, 多次计算"
  • 水平线区域: 计算资源受限
    • 如果应用程序的性能落在此区域,且远低于机器的峰值
    • 优化重点应放在充分利用处理器的计算资源上,例如: 通过向量化(SIMD指令)、多线程或更高效的并行算法设计来提高并行度,从而逼近理论峰值计算能力