[llvm-branch-commits] [llvm] [AMDGPU][SplitModule] Add entry points for unreachable call cycles (PR #230984)

Shilei Tian via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Sun Oct 11 08:06:07 PDT 2026


https://github.com/shiltian created https://github.com/llvm/llvm-project/pull/230984

A function in a call cycle always has an incoming direct call, so
`SplitGraph` never makes it an entry point. If no kernel and no other
entry point reaches the cycle, the cycle is assigned to no partition.
This can hit an assertion in `verifyGraph`.

In this PR, we visit the unreached nodes in reverse post-order of the
direct call edges, and make each node that is still unreached an entry
point. Only the outermost unreached cycles get one, so their callees
are not copied into other partitions.


>From 72f35157ae7ff1a23ceca784bf05954d6e4b73cf Mon Sep 17 00:00:00 2001
From: Shilei Tian <i at tianshilei.me>
Date: Sun, 11 Oct 2026 10:51:01 -0400
Subject: [PATCH] [AMDGPU][SplitModule] Add entry points for unreachable call
 cycles

A function in a call cycle always has an incoming direct call, so
`SplitGraph` never makes it an entry point. If no kernel and no other
entry point reaches the cycle, the cycle is assigned to no partition.
This can hit an assertion in `verifyGraph`.

In this PR, we visit the unreached nodes in reverse post-order of the
direct call edges, and make each node that is still unreached an entry
point. Only the outermost unreached cycles get one, so their callees
are not copied into other partitions.
---
 llvm/lib/Target/AMDGPU/AMDGPUSplitModule.cpp  | 41 ++++++++++++++++++-
 .../AMDGPU/non-kernels-unreachable-cycles.ll  | 21 +++++++---
 2 files changed, 55 insertions(+), 7 deletions(-)

diff --git a/llvm/lib/Target/AMDGPU/AMDGPUSplitModule.cpp b/llvm/lib/Target/AMDGPU/AMDGPUSplitModule.cpp
index 702383b76d1ee..e4cf47dbfad94 100644
--- a/llvm/lib/Target/AMDGPU/AMDGPUSplitModule.cpp
+++ b/llvm/lib/Target/AMDGPU/AMDGPUSplitModule.cpp
@@ -576,14 +576,53 @@ void SplitGraph::buildGraph(CallGraph &CG) {
       CandidateEntryPoints.push_back(N);
   }
 
+  BitVector ReachableNodes = NodesReachableByKernels;
   for (Node *N : CandidateEntryPoints) {
     // This can be another entry point if it's not reachable by a kernel
     // TODO: We could sort all of the possible new entries in a stable order
     // (e.g. by cost), then consume them one by one until
     // NodesReachableByKernels is all 1s. It'd allow us to avoid
     // considering some nodes as non-entries in some specific cases.
-    if (!NodesReachableByKernels.test(N->getID()))
+    if (!NodesReachableByKernels.test(N->getID())) {
       N->markAsGraphEntry();
+      N->getDependencies(ReachableNodes);
+    }
+  }
+
+  // A function in a call cycle always has an incoming DirectCall edge, so it
+  // is never a candidate above. If no entry point reaches a cycle, one of its
+  // functions must become an entry point. Visit the unreached nodes in reverse
+  // post-order of the DirectCall edges, so that callers come before callees.
+  // This way, a function that an unreached cycle calls does not become an
+  // entry point of its own.
+  SmallVector<Node *> PostOrder;
+  BitVector Visited = ReachableNodes;
+  SmallVector<std::pair<Node *, edges_iterator>> Stack;
+  for (Node *Root : Nodes) {
+    if (Visited.test(Root->getID()))
+      continue;
+    Visited.set(Root->getID());
+    Stack.emplace_back(Root, Root->outgoing_edges().begin());
+    while (!Stack.empty()) {
+      auto &[N, It] = Stack.back();
+      if (It == N->outgoing_edges().end()) {
+        PostOrder.push_back(N);
+        Stack.pop_back();
+        continue;
+      }
+      const Edge *E = *It++;
+      if (E->Kind != EdgeKind::DirectCall || Visited.test(E->Dst->getID()))
+        continue;
+      Visited.set(E->Dst->getID());
+      Stack.emplace_back(E->Dst, E->Dst->outgoing_edges().begin());
+    }
+  }
+
+  for (Node *N : reverse(PostOrder)) {
+    if (ReachableNodes.test(N->getID()))
+      continue;
+    N->markAsGraphEntry();
+    N->getDependencies(ReachableNodes);
   }
 
 #ifndef NDEBUG
diff --git a/llvm/test/tools/llvm-split/AMDGPU/non-kernels-unreachable-cycles.ll b/llvm/test/tools/llvm-split/AMDGPU/non-kernels-unreachable-cycles.ll
index 4c8febcb0f158..3b5e5e36b368e 100644
--- a/llvm/test/tools/llvm-split/AMDGPU/non-kernels-unreachable-cycles.ll
+++ b/llvm/test/tools/llvm-split/AMDGPU/non-kernels-unreachable-cycles.ll
@@ -1,12 +1,21 @@
-; REQUIRES: asserts
-; RUN: not --crash llvm-split -o %t -j 3 -mtriple=amdgcn-amd-amdhsa < %s 2>&1 | FileCheck --check-prefix=CRASH %s
+; RUN: llvm-split -o %t -j 3 -mtriple=amdgcn-amd-amdhsa < %s
+; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 --implicit-check-not=define %s
+; RUN: llvm-dis -o - %t1 | FileCheck --check-prefix=CHECK1 --implicit-check-not=define %s
+; RUN: llvm-dis -o - %t2 | FileCheck --check-prefix=CHECK2 --implicit-check-not=define %s
 
 ; No entry point reaches @self_rec or the cycle of @mutual_a and @mutual_b, and
-; each of these functions has an incoming direct call. FIXME: These cycles get
-; no entry point, so the graph verifier fails. Without assertions, no partition
-; defines these functions.
+; each of these functions has an incoming direct call. Check that each cycle is
+; still assigned to a partition. Only @self_rec calls @helper, and @helper is
+; defined first. Check that @helper does not become an entry point of its own,
+; so it is defined only once.
 
-; CRASH: not all nodes are reachable through the graph's entry points!
+; CHECK0: define amdgpu_kernel void @kernel
+
+; CHECK1: define void @mutual_a
+; CHECK1: define void @mutual_b
+
+; CHECK2: define internal void @helper
+; CHECK2: define void @self_rec
 
 define internal void @helper(ptr %p) {
   store i32 0, ptr %p



More information about the llvm-branch-commits mailing list