收敛性和一致性

简介

在某些环境中,线程组并行执行相同的程序,其中使用称为收敛操作的特殊原语建立组内高效通信。收敛操作的结果对参与它的线程集敏感。

收敛的直观图景是围绕着“锁步”执行的线程构建的——一组线程被认为是收敛的,如果它们都在“一起执行相同的指令序列”。这些线程可能在发散分支发散,并且它们可能稍后在某个公共程序点重新收敛

在这个直观的图景中,当收敛的线程执行指令时,如果结果值在这些线程中是相同的,则称该结果值为一致的,否则为发散的。相应地,如果分支的条件是一致的,则称该分支为一致分支,否则为发散分支。

但是,锁步执行的假设对于描述收敛操作的通信不是必要的。它还通过过度指定线程在这种并行环境中如何执行来约束实现(编译器以及硬件)。为了消除这个假设

  • 我们将收敛性定义为不同线程执行每条指令之间的关系,而不是线程本身之间的关系。这个定义对于已知目标是合理的,并且与 LLVM IR 中收敛操作的语义兼容。

  • 我们还根据这种收敛性定义一致性。只有当指令的相应执行是收敛的时,才能跨多个线程检查指令的输出是否一致。

本文档描述了一种静态分析,用于确定函数中每条指令的收敛性。该分析扩展了先前关于发散分析的工作[DivergenceSPMD],以涵盖不可归约的控制流。所描述的分析在 LLVM 中用于实现 UniformityAnalysis,该分析确定 LLVM IR 或 MIR 函数中每条指令计算的值的一致性。

[DivergenceSPMD]

Julian Rosemann、Simon Moll 和 Sebastian Hack。2021 年。可归约控制流图上 SPMD 发散的抽象解释。Proc. ACM Program。Lang. 5, POPL, Article 31 (2021 年 1 月), 35 页。 https://doi.org/10.1145/3434312

动机

发散分支约束了程序转换,例如更改 CFG 或将收敛操作移动到 CFG 的不同点。跨发散分支执行这些转换可能会更改收敛执行收敛操作的线程集。虽然这些约束不在本文档的范围之内,但一致性分析允许这些转换识别一致分支,在这些分支中这些约束不成立。

一致性本身在以共享执行资源(例如 waves、warps 或 subgroups)的组执行线程的目标上也很有用

  • 一致的输出可能在共享资源上计算或存储。

  • 这些目标必须“线性化”发散分支,以确保分支的每一侧都由同一组中的相应线程跟随。但是,在一致分支处线性化是不必要的,因为整个线程组要么遵循分支的一侧,要么遵循另一侧。

术语

循环

LLVM 循环术语中描述。

闭合路径

闭合路径和循环中描述。

不相交路径

如果 CFG 中的两条路径唯一共同的节点是起始节点或结束节点,或两者都是,则称这两条路径是不相交的。

汇合节点

分支的汇合节点是从该分支开始沿不相交路径可到达的节点。

发散路径

发散路径是从发散分支开始的路径,要么到达分支的汇合节点,要么到达函数的末尾,而不经过分支的任何汇合节点。

线程和动态实例

程序源代码中指令的每次出现都称为静态实例。当线程执行程序时,静态实例的每次执行都会产生该指令的不同的动态实例

每个线程产生唯一的动态实例序列

  • 该序列是沿着分支决策和循环遍历生成的。

  • 以“第一个”指令的动态实例开始。

  • 继续执行连续的“下一个”指令的动态实例。

线程是独立的;一些目标可以选择以组的形式执行它们,以便在可能的情况下共享资源。

_images/convergence-natural-loop.png

1

2

3

4

5

6

7

8

9

线程 1

Entry1

H1

B1

L1

H3

L3

Exit

线程 2

Entry1

H2

L2

H4

B2

L4

H5

B3

L5

Exit

在上表中,每一行是一个不同的线程,从左到右列出该线程产生的动态实例。每个线程执行相同的程序,该程序以 Entry 节点开始,以 Exit 节点结束,但是不同的线程可能会采用不同的路径通过程序的控制流。列的编号仅为方便起见,空单元格没有特殊含义。同一列中列出的动态实例是收敛的。

收敛性

先收敛是动态实例上的严格偏序,定义为以下各项的传递闭包

  1. 如果动态实例 P 在同一线程中严格在 Q 之前执行,则 P 先收敛于 Q

  2. 如果动态实例 P 在同一线程中严格在 Q1 之前执行,并且 Q1Q2 收敛,则 P 先收敛于 Q2

  3. 如果动态实例 P1P2 收敛,并且 P2 在同一线程中严格在 Q 之前执行,则 P1 先收敛于 Q

1

2

3

4

5

6

7

8

9

线程 1

Entry

S2

T

Exit

线程 2

Entry

Q2

R

S1

Exit

线程 3

Entry

P

Q1

上表显示了来自不同线程的动态实例的部分序列。同一列中的动态实例被假定为收敛的(即,在收敛关系中彼此相关)。结果收敛顺序包括边 P -> Q2Q1 -> RP -> RP -> T 等。

收敛不同线程同一静态实例产生的动态实例上的传递对称关系。

收敛关系提供任何一个定义是不切实际的,因为不同的环境可能希望以不同的方式关联动态实例。先收敛是严格偏序这一事实是对收敛关系的约束。如果不同的动态实例永远不收敛,则可以轻松满足此条件。下面,我们提供一个称为最大收敛的关系,该关系满足先收敛,并且适用于已知目标。

注意

  1. 先收敛关系不是直接可观察的。程序转换通常可以自由更改指令的顺序,即使这显然会更改先收敛关系。

  2. 收敛的动态实例不需要同时执行,甚至不需要在同一资源上执行。收敛操作的收敛动态实例可能看起来是这样做的,但这只是实现细节。

  3. P 先收敛于 Q 这一事实并不自动意味着 P 在内存模型意义上先于 Q 发生。

最大收敛性

本节定义了一个约束,该约束可用于生成最大收敛关系,而不会违反严格的先收敛顺序。这种最大收敛关系对于实际目标是合理的,并且与收敛操作兼容。

最大收敛关系是根据循环头定义的,假设线程在循环的每次“迭代”中在循环头处收敛。非正式地,如果两个线程在进入循环后都先前执行了循环头相同次数,则它们执行循环的相同迭代。一般来说,这还需要考虑父循环的迭代。

最大收敛

由不同线程为同一静态实例 X 产生的动态实例 X1X2 在最大收敛关系中收敛,当且仅当

  • X 不包含在任何循环中,或者,

  • 对于每个包含 X 的、以 H 为头的循环 C

    • 在相应线程中先于 X1H 的每个动态实例 H1 都先收敛于 X2,并且,

    • 在相应线程中先于 X2H 的每个动态实例 H2 都先收敛于 X1

    • 不假设 X1X2 收敛。

注意

如果 CFG 是不可归约的,则循环头对于给定的 CFG 可能不是唯一的。同一 CFG 的每个循环层次结构都会导致不同的最大收敛关系。

为了简洁起见,文档的其余部分将术语收敛限制为表示“在给定循环层次结构的最大收敛关系下相关”。

现在可以在前面的示例中演示最大收敛,如下所示

1

2

3

4

5

6

7

8

9

线程 1

Entry1

H1

B1

L1

H3

L3

Exit

线程 2

Entry2

H2

L2

H4

B2

L4

H5

B3

L5

Exit

  • Entry1Entry2 是收敛的。

  • H1H2 是收敛的。

  • B1B2 由于 H4 而不收敛,H4 不先收敛于 B1

  • H3H4 是收敛的。

  • H3 由于 H4 而不与 H5 收敛,H4 不先收敛于 H3

  • L1L2 是收敛的。

  • L3L4 是收敛的。

  • L3 由于 H5 而不与 L5 收敛,H5 不先收敛于 L3

对循环头的依赖

先收敛中的矛盾仅可能发生在位于某些循环内的两个节点之间。此类节点的动态实例可能在同一线程中交错,并且这种交错对于不同的线程可能是不同的。循环头充当最大收敛关系中的隐式收敛点。当线程执行节点 X 一次,然后再次执行它时,它必须遵循 CFG 中包含 X 的闭合路径。这样的路径必须穿过至少一个循环的头——包含整个闭合路径的最小循环。在给定线程中,X 的两个动态实例要么被至少一个循环头的执行分隔,要么 X 本身就是循环头。

考虑嵌套循环 C1C2、…、Ck 的序列,使得 C1 是最外层循环,Ck 是最内层循环,其头分别为 H1H2、…、Hk。当线程进入循环 Ck 时,以下任何一种情况都可能发生

  1. 线程直接进入循环 Ck,而没有执行任何头 H1Hk

  2. 线程执行了一些或所有嵌套头一次或多次。

最大收敛关系捕获了关于循环的以下直觉

  1. 当两个线程进入顶层循环 C1 时,它们执行作为 C1子节点的每个节点的收敛动态实例。

  2. 当两个线程进入嵌套循环 Ck 时,它们执行作为 Ck 的子节点的每个节点的收敛动态实例,直到任一线程退出 Ck 为止,当且仅当它们执行了它们任一线程遇到的最后一个嵌套头的收敛动态实例。

    请注意,当线程退出嵌套循环 Ck 时,它必须遵循 Ck 外部的闭合路径才能重新进入它。这需要执行某个外部循环的头,如前所述。

考虑由线程 T1T2 为作为嵌套循环 Ck 的子节点的节点 X 产生的两个动态实例 X1X2。最大收敛按如下方式关联 X1X2

  1. 如果两个线程都没有执行来自 H1Hk 的任何头,则 X1X2 是收敛的。

  2. 否则,如果没有任何来自 H1Hk 的任何头 Q 的收敛动态实例 Q1Q2(其中 Q 可能与 X 相同),使得 Q1 先于 X1Q2 先于相应线程中的 X2,则 X1X2 不收敛。

  3. 否则,考虑在相应线程中最近在 X1X2 之前发生的来自 H1Hk 的头 Q 的收敛动态实例对 Q1Q2。然后,当且仅当在线程 T1 中的 Q1X1 之间,或在线程 T2 中的 Q2X2 之间,没有发生来自 H1Hk 的任何头的动态实例时,X1X2 才收敛。换句话说,Q1Q2 表示最后的收敛点,在执行 X 之前没有执行其他头。

示例

_images/convergence-both-diverged-nested.png

上图显示了两个嵌套的不可归约循环,其头为 RS。节点 EntryQ 具有发散分支。下表显示了通过 CFG 中不同路径的三个线程之间的收敛性。同一列中列出的动态实例是收敛的。

1

2

3

4

5

6

7

8

10

线程1

Entry

P1

Q1

S1

P3

Q3

R1

S2

Exit

线程2

Entry

P2

Q2

R2

S3

Exit

线程3

Entry

R3

S4

Exit

  • P2P3 由于 S1 而不收敛

  • Q2Q3 由于 S1 而不收敛

  • S1S3 由于 R2 而不收敛

  • S1S4 由于 R3 而不收敛

非正式地,T1T2 执行内部循环的次数不同,而没有执行外部循环的头。当所有线程首次执行外部循环的头时,它们在外部循环中收敛。

一致性

  1. 当且仅当两个收敛的动态实例的输出对于这两个动态实例比较相等时,这两个动态实例的输出才是一致的。

  2. 静态实例 X 的输出对于给定的线程集是一致的,当且仅当对于这些线程产生的 X 的每对收敛动态实例,它都是一致的。

非一致的值称为发散的

对于线程集 S,静态实例的每个输出的一致性确定如下

  1. 指令的语义可能会指定输出是一致的。

  2. 否则,如果静态实例不是 m-收敛的,则输出是发散的。

  3. 否则,如果静态实例是 m-收敛的

    1. 如果是 PHI 节点,则当且仅当对于由 S 中所有线程产生的每对收敛动态实例时,其输出才是一致的

      1. 两个实例都从收敛的动态实例中选择相同的输出,并且,

      2. 该输出对于 S 中的所有线程都是一致的。

    2. 否则,当且仅当输入操作数对于 S 中的所有线程都是一致的时,输出才是一致的。

发散的循环出口

当发散分支发生在循环内部时,发散路径可能会继续到循环的出口。这称为发散循环出口。如果循环是不可归约的,则发散路径可能会重新进入并最终到达循环内的汇合点。应该检查这样的汇合点是否符合发散入口标准。

当两个线程在循环内收敛执行产生一致的值,但在执行循环头不同次数后(非正式地,在循环的不同迭代中)沿着相同的发散路径退出循环时,沿发散路径且位于循环外部的节点会经历时间发散。对于循环内的节点 N,两个线程的输出可能是一致的,但循环外的任何使用 U 都会接收来自 N 的非收敛动态实例的值。U 的输出可能是发散的,具体取决于指令的语义。

静态一致性分析

不可归约的控制流会导致不同的循环层次结构,具体取决于深度优先遍历期间头的选择。因此,静态分析并非总是可以确定不可归约循环中节点的收敛性,并且任何一致性分析都仅限于那些静态实例,这些静态实例的收敛性与循环层次结构无关

m-收敛静态实例

当且仅当其动态实例的最大收敛关系在可以为该 CFG 构建的每个循环层次结构中都相同时,给定 CFG 的静态实例 X 才是m-收敛的。

注意

换句话说,m-收敛静态实例 X 的两个动态实例 X1X2 在某个循环层次结构中收敛,当且仅当它们在同一 CFG 的每个其他循环层次结构中也收敛。

如前所述,为了简洁起见,我们将术语收敛限制为表示“在给定循环层次结构的最大收敛关系下相关”。

如果给定 CFG 中的每个节点 X 满足以下必要条件,则报告该节点是 m-收敛的

  1. 循环内的每个发散分支都满足发散入口标准,并且,

  2. 没有来自外部发散分支的到达循环的发散路径

注意

可归约循环轻松满足上述条件。特别是,如果整个 CFG 是可归约的,则 CFG 中的所有节点都是 m-收敛的。

使用先前描述的标准确定静态实例的每个输出的一致性。发散输出的发现可能会导致其用途(包括分支)也变得发散。分析传播这种发散,直到达到固定点。

使用这些标准推断出的收敛性是任何循环层次结构的最大收敛关系的安全子集。特别是,即使在检查某些其他循环层次结构 T' 时未检测到该事实,也足以确定静态实例对于给定循环层次结构 T 是否为 m-收敛的。

此属性允许编译器转换使用一致性分析,而不会受到底层循环分析中 DFS 选择的影响。当两个转换对同一个 CFG 使用不同的一致性分析实例时,一个分析实例中的“发散值”结果不会与另一个分析实例中的“一致值”结果相矛盾。

诸如 SimplifyCFG、CSE 和循环转换之类的通用转换通常会以更改最大收敛关系的方式更改程序。 这也意味着先前一致的值在这样的转换之后可能会变得发散。 在此类转换之后,必须重新计算一致性。

循环内的发散分支

_images/convergence-divergent-inside.png

上图显示了不可约循环区域内的发散分支 Q。当两个线程在 Q 处发散时,循环区域内动态实例的收敛取决于所选择的循环层次结构

  1. 在检测到以 P 为头的单个循环 C 的实现中,循环内的收敛由 P 决定。

  2. 在检测到以 RS 为头的两个嵌套循环的实现中,这些循环内的收敛由它们各自的头决定。

一种保守的方法是简单地将不可约循环内的所有节点报告为具有发散输出。但是,为了最大化一致性,期望识别 CFG 中的 m-收敛节点。本节描述了一种从此类闭合路径导出的节点模式,闭合路径是 CFG 的属性,不依赖于循环层次结构。

发散入口标准

仅当对于位于 P 上的每个发散分支 B 及其汇合节点 J,没有从 BJ 的发散路径上的 P 入口时,闭合路径 P 中所有节点的动态实例才是 m-收敛的。

_images/convergence-closed-path.png

考虑上图中的闭合路径 P -> Q -> R -> SPR闭合路径的入口Q 是发散分支,S 是该分支的汇合点,发散路径为 Q -> R -> SQ -> S

  • 如果存在发散入口 R,则在某些循环层次结构中,R 是包含闭合路径的最小循环 C 的头,并且在集合 C - R 中存在一个 子循环 C',其中包含分支 Q 和汇合点 S。当线程在 Q 处发散时,一个子集 M 继续在循环 C' 内,而补集 N 退出 C' 并到达 RS 的动态实例由集合 M 中的线程执行,由于 R 的存在,它们与集合 N 中执行的实例不收敛。通俗地说,在 Q 处发散的线程在外循环 C 的同一次迭代中重新收敛,但它们可能以不同的方式执行了内循环 C'

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    线程1

    Entry

    P1

    Q1

    R1

    S1

    P3

    Exit

    线程2

    Entry

    P2

    Q2

    S2

    P4

    Q4

    R2

    S4

    Exit

    在上表中,由于 R1S2S1 不收敛。


  • 如果 R 不存在,或者如果 R 以外的任何节点是 C 的头,则不会检测到这样的子循环 C'。在 Q 处发散的线程执行 S 的收敛动态实例,因为它们在从 QS 的任何路径上都不会遇到循环头。通俗地说,在 Q 处发散的线程在 C 的同一次迭代中在 S 处重新收敛。

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    线程1

    Entry

    P1

    Q1

    R1

    S1

    P3

    Q3

    R3

    S3

    Exit

    线程2

    Entry

    P2

    Q2

    S2

    P4

    Q4

    R2

    S4

    Exit


注意

一般来说,对于不同的头,上述语句中的循环 C 预计不是同一个循环。循环及其头是紧密耦合的;对于同一个最外层循环中的不同头,检测到的子循环可能不同。与上述示例相关的属性是,对于每个闭合路径,都存在一个包含该路径且其头位于该路径上的循环 C

必须为每个通过发散分支 B 及其汇合点 J 的闭合路径检查发散入口标准。由于 每个闭合路径都通过某个循环的头,这相当于检查每个包含 BJ 的循环 C。当 C 的头支配汇合点 J 时,从头到 J 的任何路径都不可能有入口,其中包括从 BJ 的任何发散路径。对于通过包含 C 的外循环的头的任何闭合路径,情况也是如此。

因此,发散入口标准可以保守地简化如下

对于发散分支 B 及其汇合节点 J,包含 BJ 的循环 C 中的节点是 m-收敛的,仅当

  • B 严格支配 J,或者,

  • C 的头 H 严格支配 J,或者,

  • 递归地,在 C 内部存在满足相同条件的循环 C'

JHB 相同时,平凡支配不足以对发散路径的入口做出任何陈述。

到达循环的发散路径

_images/convergence-divergent-outside.png

该图显示了两个循环层次结构,其中发散分支在 Entry 中而不是在 Q 中。对于分别在 PR 处进入闭合路径 P -> Q -> R -> S 的两个线程,沿路径生成的动态实例的收敛取决于 P 还是 R 是头。

  • P 是头时的收敛。

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    线程1

    Entry

    P1

    Q1

    R1

    S1

    P3

    Q3

    S3

    Exit

    线程2

    Entry

    R2

    S2

    P2

    Q2

    S2

    P4

    Q4

    R3

    S4

    Exit


  • R 是头时的收敛。

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    线程1

    Entry

    P1

    Q1

    R1

    S1

    P3

    Q3

    S3

    Exit

    线程2

    Entry

    R2

    S2

    P2

    Q2

    S2

    P4

    Exit


因此,当发散路径从循环外部到达不可约循环的不同入口时,静态分析保守地报告循环中的每个节点都不是 m-收敛的。

可约循环

如果 C 是以 H 为头的可约循环,则在任何 DFS 中,H 必须是包含 C 的某个循环 C' 的头。独立于 DFS,除了 H 本身之外,没有到子图 C 的入口。因此,我们有以下结论

  1. 对于发散分支及其汇合点,发散入口标准在子图 C 内部是显然满足的。

  2. 当发散路径从外部到达子图 C 时,它们的收敛始终由同一个头 H 决定。

显然,这只能在循环层次结构 T 中确定,其中 C 被检测为可约循环。在不同的循环层次结构 T' 中无法得出这样的结论,在 T' 中,C 是具有相同头的更大循环 C' 的一部分,但这与 T 中的结论不矛盾。

受控收敛

收敛控制令牌 为确定哪些线程在程序中的给定点收敛提供了显式语义。 这的影响体现在动态实例的 受控最大收敛关系 和静态实例的 受控 m-收敛 属性中。 LLVM 中实现的 一致性分析 包含了对支持收敛控制令牌的目标的支持。