收敛性和一致性

简介

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

收敛的直观概念是围绕线程“同步”执行构建的——如果一组线程都在“一起执行相同的指令序列”,则认为它们是收敛的。这些线程可能会在不同分支发散,之后也可能在某个公共程序点重新收敛

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

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

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

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

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

[DivergenceSPMD]

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

动机

不同分支限制了程序转换,例如更改 CFG 或将收敛操作移动到 CFG 的不同位置。跨不同分支执行这些转换可能会更改以收敛方式执行收敛操作的线程集。虽然这些约束不在本文档的范围之内,但一致性分析允许这些转换识别不满足这些约束的一致分支。

一致性本身在以组方式(例如波、线或子组)执行线程并共享执行资源的目标上也很有用。

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

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

术语

循环

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 不收敛于 B1 之前。

  • H3H4 收敛。

  • H3 未与 H5 收敛,因为 H4 不收敛于 H3 之前。

  • L1L2 收敛。

  • L3L4 收敛。

  • L3 未与 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(其中 Q 可能与 X 相同)的收敛动态实例 Q1Q2,使得 Q1 先于 X1Q2 先于 X2 在各自的线程中,则 X1X2 不收敛。

  3. 否则,考虑从 H1Hk 的头部 Q 的收敛动态实例对 Q1Q2,它们在各自的线程中最近出现在 X1X2 之前。然后,当且仅当不存在任何从 H1Hk 的头部的动态实例出现在线程 T1Q1X1 之间,或者出现在线程 T2Q2X2 之间时,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 的输出对于这些线程产生的每个收敛的动态实例对都是一致的,则其输出对于给定的一组线程来说是一致的。

非一致的值被称为发散

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

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

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

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

    1. 如果它是一个 PHI 节点,则当且仅当对于所有线程 S 中产生的每个收敛的动态实例对

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

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

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

发散循环出口

当在循环内部发生发散分支时,发散路径可能会继续到循环的出口。这称为发散循环出口。如果循环是不可约的,则发散路径可能会重新进入并最终到达循环内的连接点。应检查此类连接点是否满足 发散入口准则

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

静态均匀性分析

不可约控制流会导致不同的循环层次结构,具体取决于深度优先遍历期间头节点的选择。因此,静态分析无法始终确定不可约循环中节点的收敛性,任何均匀性分析都仅限于那些收敛性独立于循环层次结构的静态实例。

m-收敛静态实例

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

注意

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

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

如果且仅当给定 CFG 中的每个节点 X 包含的每个循环都满足以下必要条件,则报告该节点为 m-收敛的

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

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

注意

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

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

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

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

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

循环内的发散分支

_images/convergence-divergent-inside.png

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

  1. 在检测到具有头节点 P 的单个循环 C 的实现中,循环内部的收敛性由 P 确定。

  2. 在检测到具有头节点 RS 的两个嵌套循环的实现中,这些循环内部的收敛性由其各自的头节点确定。

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

发散入口准则

闭合路径 P 中所有节点的动态实例仅当对于每个发散分支 B 及其连接节点 J(位于 P 上)来说,不存在位于从 BJ 的发散路径上的 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' 并到达 R。由于 R 的存在,子集 M 中的线程执行的 S 的动态实例与子集 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 处发散的线程在 S 处在 C 的相同迭代中重新收敛。

    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的线程,沿路径生成的动态实例的收敛性取决于PR是否为头节点。

  • 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的可约循环,那么在任何深度优先搜索中,H必须是包含C的某个循环C'的头节点。与深度优先搜索无关,除了H本身之外,子图C不存在其他入口。因此,我们有以下结论:

  1. 对于一个发散分支及其连接点,如果两者都位于子图C内部,则发散入口准则显然满足。

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

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

受控收敛

收敛控制令牌提供了一种明确的语义来确定程序中给定点哪些线程是收敛的。这种影响体现在受控最大收敛关系(在动态实例之间)和受控m-收敛(静态实例的属性)上。LLVM 中实现的一致性分析包含了这一点,适用于支持收敛控制令牌的目标。