[AMDGPU] Consider safely coalescing acyclic same-predicate triangle chains with live-out PHIs
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
I would like to discuss a narrowly scoped CFG transformation for consecutive single-arm diamonds (triangles) guarded by exactly the same SSA predicate. This pattern occurs in AMDGPU workloads and can leave redundant divergent branches.
This is **not** a proposal to enable `JumpThreadingPass` for divergent targets or broadly relax SimplifyCFG's live-out restriction. The transformation should apply only after proving the complete CFG, SSA, and convergence safety conditions below.
## Motivating example
Simplified input:
```llvm
define i32 @same_predicate(ptr %a, ptr %b, i1 noundef %p) {
entry:
br i1 %p, label %load.a, label %join.a
load.a:
%va = load i32, ptr %a, align 4
br label %join.a
join.a:
%a.value = phi i32 [ %va, %load.a ], [ 0, %entry ]
br i1 %p, label %load.b, label %join.b
load.b:
%vb = load i32, ptr %b, align 4
br label %join.b
join.b:
%b.value = phi i32 [ %vb, %load.b ], [ 0, %join.a ]
%result = add i32 %a.value, %b.value
ret i32 %result
}
```
The second branch is redundant because it uses the same `%p`, but `%a.value` is live beyond `join.a`. This prevents the existing generic SimplifyCFG threading from redirecting the edge.
A possible result is:
```llvm
define i32 @same_predicate(ptr %a, ptr %b, i1 noundef %p) {
entry:
br i1 %p, label %load.a, label %join.b
load.a:
%va = load i32, ptr %a, align 4
br label %load.b
load.b:
%vb = load i32, ptr %b, align 4
br label %join.b
join.b:
%a.value = phi i32 [ %va, %load.b ], [ 0, %entry ]
%b.value = phi i32 [ %vb, %load.b ], [ 0, %entry ]
%result = add i32 %a.value, %b.value
ret i32 %result
}
```
For `%p == true`, both versions execute `load.a` followed by `load.b`. For `%p == false`, both loads are skipped and both PHIs select their fallback values.
## Conservative safety boundary
The transformation would be attempted only when all of the following are proven:
- Every branch uses the exact same `Value *` condition and the same active-successor polarity.
- Each body has one predecessor and one unconditional successor, each join has exactly the expected two predecessors, and there are no side entries or exits.
- Intermediate joins contain only PHIs, debug information, and the conditional branch, and no removed block has its address taken.
- The complete candidate is outside a cycle. Initially, the transform should be rejected whenever the final join may reach the first head.
- Initially, bodies contain only side-effect-free instructions and simple non-volatile loads. All `CallBase`, writes, volatile or atomic operations, fences, barriers, inline assembly, and convergence- or WQM/WWM-sensitive intrinsics are rejected.
- Every moved PHI incoming value dominates its new incoming edge.
- A moved PHI has no semantic use before the final join and is not used as an incoming value of another PHI at that join.
- Existing PHIs in the final join can have their bypass predecessor remapped without introducing a dominance violation.
These instruction restrictions are deliberately conservative and can be reconsidered separately after the basic transformation is established.
For example, this case must not be transformed by merely moving `%a.value` to `join.b`:
```llvm
join.a:
%a.value = phi i32 [ %va, %load.a ], [ 0, %entry ]
br i1 %p, label %load.b, label %join.b
load.b:
%use.before.final.join = add i32 %a.value, 1
br label %join.b
```
After the move, `%a.value` would no longer dominate its use in `load.b`. PHI operands require the same edge-sensitive treatment.
## Possible implementation direction
One possible implementation is a dedicated `SimplifyCFG` helper, called only when existing predecessor-known threading makes no change:
1. Collect a maximal eligible chain without mutating the IR.
2. Validate all structural, dominance, PHI-use, cycle, and instruction conditions.
3. Move the existing PHIs to the final join, remap incoming blocks, and replace redundant branches.
4. Apply dominator-tree updates together and let normal cleanup remove trivial blocks.
This should require neither `SSAUpdater`, instruction cloning, nor new PHIs. A single whole-chain commit also avoids repeatedly moving accumulated PHIs.
`FlattenCFG` could instead provide a target-scoped fallback if the pattern is shown to arise too late for SimplifyCFG. The logic should not be duplicated in both places.
## Safety and validation
Before enabling this in a default pipeline, the patch should include:
- Positive and negative IR tests covering both polarities, multiple PHIs, malformed CFGs, PHI edge uses, cycles, side effects, convergence, and WQM/WWM operations.
- `-verify-each`, dominator-tree, debug-info, and long-chain compile-time coverage.
- An AMDGPU end-to-end test showing that the redundant mask branch is removed without changing convergence-sensitive cases.
- Alive2 checks for target-independent semantics, with separate AMDGPU tests for convergence and EXEC-mask behavior.
Does this sufficiently narrow the legality boundary, and does `SimplifyCFG` seem like the appropriate owner for such a transformation?
Contributor guide
Research direction
Start by reading SimplifyCFG's existing predecessor-known threading and compare it with the proposed dedicated helper; FlattenCFG is mentioned as an alternative owner. Done requires a validated whole-chain transformation plus positive and negative IR tests covering PHIs, cycles, side effects, convergence, and WQM/WWM operations, along with verifier, dominator-tree, debug-info, compile-time, and AMDGPU validation.
Written by the indexing model from the issue text.
Assessment
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100