[llvm] [NVPTXAsmPrinter] Allow self-referential device global initializers (PR #197838)

via llvm-commits llvm-commits at lists.llvm.org
Thu Jul 23 08:05:11 PDT 2026


https://github.com/aryanmagoon updated https://github.com/llvm/llvm-project/pull/197838

>From e13267f64c6fbfe7392cab0b7de9f0e9162f00b5 Mon Sep 17 00:00:00 2001
From: aryanmagoon <amagoon at nvidia.com>
Date: Tue, 21 Jul 2026 18:57:01 +0000
Subject: [PATCH 1/2] [NVPTX] Support cyclic global initializers with forward
 declarations

NVPTX emits global definitions in dependency order so that symbols referenced by initializers have already been seen by ptxas. The existing recursive walk reports every back edge as a circular dependency, rejecting self-referential and mutually referential device globals.

Model the module GlobalVariables as a dependency graph and use scc_iterator to visit strongly connected components in dependency-first order. For a cyclic component, emit compatible .extern declarations, then order the remaining definitions after treating those declarations as satisfied.

Reuse the definition emitter for forward declarations so address spaces, alignments, scalar types, and aggregate layouts match exactly. Internal-only cycles still produce the existing fatal error because a PTX .extern declaration cannot be resolved by a static definition.

The declaration form works with both newer ptxas and older versions such as CUDA 13.2, which require a declaration even for a self-reference. Add coverage for self-references, visible and weak cycles, mixed-linkage cycles, dependency-first SCC ordering, packed aggregates, and unrepresentable internal-only cycles.
---
 llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp     | 294 +++++++++++++-----
 llvm/lib/Target/NVPTX/NVPTXAsmPrinter.h       |   4 +
 .../CodeGen/NVPTX/global-cycle-internal.ll    |  10 +
 llvm/test/CodeGen/NVPTX/global-cycle.ll       |  48 +++
 llvm/test/CodeGen/NVPTX/global-ordering.ll    |   7 +
 .../CodeGen/NVPTX/packed-aggr-self-ptx70.ll   |  11 +
 llvm/test/CodeGen/NVPTX/packed-aggr.ll        |  15 +
 7 files changed, 314 insertions(+), 75 deletions(-)
 create mode 100644 llvm/test/CodeGen/NVPTX/global-cycle-internal.ll
 create mode 100644 llvm/test/CodeGen/NVPTX/global-cycle.ll
 create mode 100644 llvm/test/CodeGen/NVPTX/packed-aggr-self-ptx70.ll

diff --git a/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp b/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp
index a22e2cd05a931..b120850f7da85 100644
--- a/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp
+++ b/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp
@@ -32,6 +32,8 @@
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/SCCIterator.h"
+#include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/Sequence.h"
 #include "llvm/ADT/SmallString.h"
@@ -89,9 +91,12 @@
 #include "llvm/Target/TargetLoweringObjectFile.h"
 #include "llvm/Target/TargetMachine.h"
 #include "llvm/Transforms/Utils/UnrollLoop.h"
+#include <algorithm>
 #include <cassert>
 #include <cstdint>
 #include <cstring>
+#include <map>
+#include <set>
 #include <string>
 
 using namespace llvm;
@@ -137,51 +142,157 @@ static void emitInitialRawDwarfLocDirective(const MachineFunction &MF,
   (void)DD->emitInitialLocDirective(MF, /*CUID=*/0);
 }
 
-/// discoverDependentGlobals - Return a set of GlobalVariables on which \p V
-/// depends.
-static void
-discoverDependentGlobals(const Value *V,
-                         DenseSet<const GlobalVariable *> &Globals) {
+namespace {
+
+/// Return a list of GlobalVariables on which \p V depends.
+static void discoverDependentGlobals(
+    const Value *V, SmallVectorImpl<const GlobalVariable *> &Globals,
+    SmallPtrSetImpl<const GlobalVariable *> &Seen) {
   if (const GlobalVariable *GV = dyn_cast<GlobalVariable>(V)) {
-    Globals.insert(GV);
+    if (Seen.insert(GV).second)
+      Globals.push_back(GV);
     return;
   }
 
   if (const User *U = dyn_cast<User>(V))
     for (const auto &O : U->operands())
-      discoverDependentGlobals(O, Globals);
-}
-
-/// VisitGlobalVariableForEmission - Add \p GV to the list of GlobalVariable
-/// instances to be emitted, but only after any dependents have been added
-/// first.s
-static void
-VisitGlobalVariableForEmission(const GlobalVariable *GV,
-                               SmallVectorImpl<const GlobalVariable *> &Order,
-                               DenseSet<const GlobalVariable *> &Visited,
-                               DenseSet<const GlobalVariable *> &Visiting) {
-  // Have we already visited this one?
-  if (Visited.count(GV))
-    return;
+      discoverDependentGlobals(O, Globals, Seen);
+}
 
-  // Do we have a circular dependency?
-  if (!Visiting.insert(GV).second)
-    report_fatal_error("Circular dependency found in global variable set");
+struct GlobalVariableDependencyNode {
+  const GlobalVariable *GV = nullptr;
+  unsigned ModuleOrder = 0;
+  SmallVector<const GlobalVariableDependencyNode *, 4> Dependencies;
+};
+
+class GlobalVariableDependencyGraph {
+  // scc_iterator needs a single entry node. Global initializer dependencies
+  // may be disconnected, so use a synthetic root with an edge to every global.
+  GlobalVariableDependencyNode SyntheticRoot;
+  std::map<const GlobalVariable *, GlobalVariableDependencyNode> Nodes;
+
+public:
+  explicit GlobalVariableDependencyGraph(const Module &M) {
+    unsigned ModuleOrder = 0;
+    for (const GlobalVariable &GV : M.globals()) {
+      GlobalVariableDependencyNode &Node = Nodes.try_emplace(&GV).first->second;
+      Node.GV = &GV;
+      Node.ModuleOrder = ModuleOrder++;
+      SyntheticRoot.Dependencies.push_back(&Node);
+    }
+
+    for (auto &[GV, Node] : Nodes) {
+      SmallVector<const GlobalVariable *, 4> Dependencies;
+      SmallPtrSet<const GlobalVariable *, 4> Seen;
+      for (const Use &Operand : GV->operands())
+        discoverDependentGlobals(Operand, Dependencies, Seen);
+
+      for (const GlobalVariable *Dependency : Dependencies) {
+        auto It = Nodes.find(Dependency);
+        if (It != Nodes.end())
+          Node.Dependencies.push_back(&It->second);
+      }
+    }
+  }
+
+  const GlobalVariableDependencyNode *getEntryNode() const {
+    return &SyntheticRoot;
+  }
+};
+
+struct GlobalVariableDependencyGraphTraits {
+  using NodeRef = const GlobalVariableDependencyNode *;
+  using ChildIteratorType =
+      SmallVectorImpl<const GlobalVariableDependencyNode *>::const_iterator;
 
-  // Make sure we visit all dependents first
-  DenseSet<const GlobalVariable *> Others;
-  for (const auto &O : GV->operands())
-    discoverDependentGlobals(O, Others);
+  static NodeRef getEntryNode(NodeRef Node) { return Node; }
+  static ChildIteratorType child_begin(NodeRef Node) {
+    return Node->Dependencies.begin();
+  }
+  static ChildIteratorType child_end(NodeRef Node) {
+    return Node->Dependencies.end();
+  }
+};
 
-  for (const GlobalVariable *GV : Others)
-    VisitGlobalVariableForEmission(GV, Order, Visited, Visiting);
+using GlobalVariableSCCIterator =
+    scc_iterator<const GlobalVariableDependencyNode *,
+                 GlobalVariableDependencyGraphTraits>;
 
-  // Now we can visit ourself
-  Order.push_back(GV);
-  Visited.insert(GV);
-  Visiting.erase(GV);
+static bool shouldSkipModuleLevelGlobal(const GlobalVariable &GV) {
+  if (GV.hasSection() && GV.getSection() == "llvm.metadata")
+    return true;
+  return GV.getName().starts_with("llvm.") ||
+         GV.getName().starts_with("nvvm.");
 }
 
+static bool isForwardDeclarableGlobal(const GlobalVariable *GVar) {
+  if (shouldSkipModuleLevelGlobal(*GVar) || GVar->isDeclaration() ||
+      getPTXOpaqueType(*GVar) != PTXOpaqueType::None)
+    return false;
+
+  // A PTX .extern declaration can be resolved by a later .visible, .weak, or
+  // .common definition, but not by a static definition.
+  if (GVar->hasExternalLinkage())
+    return GVar->hasInitializer();
+
+  if (GVar->hasLinkOnceLinkage() || GVar->hasWeakLinkage() ||
+      GVar->hasAvailableExternallyLinkage() || GVar->hasCommonLinkage())
+    return true;
+
+  return false;
+}
+
+/// Order definitions after treating references to forward-declared globals as
+/// already satisfied. A remaining cycle cannot be emitted portably because it
+/// requires an undeclared forward reference.
+static SmallVector<const GlobalVariable *, 4>
+orderDefinitionsInSCC(
+    ArrayRef<const GlobalVariableDependencyNode *> SCC,
+    const DenseSet<const GlobalVariableDependencyNode *> &ForwardDeclared) {
+  using Node = GlobalVariableDependencyNode;
+
+  DenseSet<const Node *> SCCSet;
+  SCCSet.insert_range(SCC);
+
+  DenseMap<const Node *, unsigned> DependencyCount;
+  DenseMap<const Node *, SmallVector<const Node *, 4>> Dependents;
+  std::set<std::pair<unsigned, const Node *>> Ready;
+
+  for (const Node *N : SCC) {
+    unsigned &Count = DependencyCount[N];
+    for (const Node *Dependency : N->Dependencies) {
+      if (!SCCSet.count(Dependency) || ForwardDeclared.count(Dependency))
+        continue;
+      ++Count;
+      Dependents[Dependency].push_back(N);
+    }
+    if (Count == 0)
+      Ready.emplace(N->ModuleOrder, N);
+  }
+
+  SmallVector<const GlobalVariable *, 4> Order;
+  while (!Ready.empty()) {
+    const Node *N = Ready.begin()->second;
+    Ready.erase(Ready.begin());
+    Order.push_back(N->GV);
+
+    auto It = Dependents.find(N);
+    if (It == Dependents.end())
+      continue;
+    for (const Node *Dependent : It->second) {
+      assert(DependencyCount[Dependent] && "Dependency already satisfied");
+      if (--DependencyCount[Dependent] == 0)
+        Ready.emplace(Dependent->ModuleOrder, Dependent);
+    }
+  }
+
+  if (Order.size() != SCC.size())
+    report_fatal_error("Circular dependency found in global variable set");
+  return Order;
+}
+
+} // namespace
+
 void NVPTXAsmPrinter::emitInstruction(const MachineInstr *MI) {
   NVPTX_MC::verifyInstructionPredicates(MI->getOpcode(),
                                         getSubtargetInfo().getFeatureBits());
@@ -842,28 +953,53 @@ void NVPTXAsmPrinter::emitGlobals(const Module &M) {
 
   emitDeclarations(M, OS2);
 
-  // As ptxas does not support forward references of globals, we need to first
-  // sort the list of module-level globals in def-use order. We visit each
-  // global variable in order, and ensure that we emit it *after* its dependent
-  // globals. We use a little extra memory maintaining both a set and a list to
-  // have fast searches while maintaining a strict ordering.
-  SmallVector<const GlobalVariable *, 8> Globals;
-  DenseSet<const GlobalVariable *> GVVisited;
-  DenseSet<const GlobalVariable *> GVVisiting;
+  const NVPTXTargetMachine &NTM = static_cast<const NVPTXTargetMachine &>(TM);
+  const NVPTXSubtarget &STI = *NTM.getSubtargetImpl();
 
-  // Visit each global variable, in order
-  for (const GlobalVariable &I : M.globals())
-    VisitGlobalVariableForEmission(&I, Globals, GVVisited, GVVisiting);
+  // ptxas requires global symbols referenced by initializers to be known
+  // before use. Acyclic dependencies can be handled by dependency-first
+  // emission. Cyclic SCCs need compatible .extern declarations first.
+  GlobalVariableDependencyGraph DependencyGraph(M);
+  for (GlobalVariableSCCIterator I =
+           GlobalVariableSCCIterator::begin(DependencyGraph.getEntryNode());
+       !I.isAtEnd(); ++I) {
+    SmallVector<const GlobalVariableDependencyNode *, 4> SCC(I->begin(),
+                                                              I->end());
+
+    // Nothing points to the synthetic root, so it is always in its own SCC.
+    if (!SCC.front()->GV) {
+      assert(SCC.size() == 1 && "Synthetic root must be in its own SCC");
+      continue;
+    }
 
-  assert(GVVisited.size() == M.global_size() && "Missed a global variable");
-  assert(GVVisiting.size() == 0 && "Did not fully process a global variable");
+    llvm::sort(SCC, [](const auto *LHS, const auto *RHS) {
+      return LHS->ModuleOrder < RHS->ModuleOrder;
+    });
 
-  const NVPTXTargetMachine &NTM = static_cast<const NVPTXTargetMachine &>(TM);
-  const NVPTXSubtarget &STI = *NTM.getSubtargetImpl();
+    const bool IsCyclic = I.hasCycle();
+    DenseSet<const GlobalVariableDependencyNode *> ForwardDeclared;
+    if (IsCyclic)
+      for (const auto *Node : SCC)
+        if (isForwardDeclarableGlobal(Node->GV))
+          ForwardDeclared.insert(Node);
 
-  // Print out module-level global variables in proper order
-  for (const GlobalVariable *GV : Globals)
-    printModuleLevelGV(GV, OS2, /*ProcessDemoted=*/false, STI);
+    // Check that declarations break every cycle before writing any output.
+    SmallVector<const GlobalVariable *, 4> OrderedGlobals =
+        IsCyclic ? orderDefinitionsInSCC(SCC, ForwardDeclared)
+                 : SmallVector<const GlobalVariable *, 4>{SCC.front()->GV};
+
+    for (const auto *Node : SCC) {
+      if (!ForwardDeclared.count(Node))
+        continue;
+      OS2 << ".extern ";
+      emitPTXGlobalVariableDefinition(Node->GV, OS2, STI,
+                                      /*EmitInitializer=*/false);
+      OS2 << ";\n";
+    }
+
+    for (const GlobalVariable *GV : OrderedGlobals)
+      printModuleLevelGV(GV, OS2, /*ProcessDemoted=*/false, STI);
+  }
 
   OS2 << '\n';
 
@@ -980,21 +1116,10 @@ void NVPTXAsmPrinter::emitLinkageDirective(const GlobalValue *V,
 void NVPTXAsmPrinter::printModuleLevelGV(const GlobalVariable *GVar,
                                          raw_ostream &O, bool ProcessDemoted,
                                          const NVPTXSubtarget &STI) {
-  // Skip meta data
-  if (GVar->hasSection())
-    if (GVar->getSection() == "llvm.metadata")
-      return;
-
-  // Skip LLVM intrinsic global variables
-  if (GVar->getName().starts_with("llvm.") ||
-      GVar->getName().starts_with("nvvm."))
+  // Skip metadata and LLVM intrinsic global variables.
+  if (shouldSkipModuleLevelGlobal(*GVar))
     return;
 
-  const DataLayout &DL = getDataLayout();
-
-  // GlobalVariables are always constant pointers themselves.
-  Type *ETy = GVar->getValueType();
-
   if (GVar->hasExternalLinkage()) {
     if (GVar->hasInitializer())
       O << ".visible ";
@@ -1109,6 +1234,17 @@ void NVPTXAsmPrinter::printModuleLevelGV(const GlobalVariable *GVar,
     return;
   }
 
+  emitPTXGlobalVariableDefinition(GVar, O, STI, /*EmitInitializer=*/true);
+  O << ";\n";
+}
+
+void NVPTXAsmPrinter::emitPTXGlobalVariableDefinition(
+    const GlobalVariable *GVar, raw_ostream &O, const NVPTXSubtarget &STI,
+    bool EmitInitializer) {
+  const DataLayout &DL = getDataLayout();
+
+  Type *ETy = GVar->getValueType();
+
   O << ".";
   emitPTXAddressSpace(GVar->getAddressSpace(), O);
 
@@ -1135,7 +1271,7 @@ void NVPTXAsmPrinter::printModuleLevelGV(const GlobalVariable *GVar,
 
     // Ptx allows variable initilization only for constant and global state
     // spaces.
-    if (GVar->hasInitializer()) {
+    if (EmitInitializer && GVar->hasInitializer()) {
       if ((GVar->getAddressSpace() == ADDRESS_SPACE_GLOBAL) ||
           (GVar->getAddressSpace() == ADDRESS_SPACE_CONST)) {
         const Constant *Initializer = GVar->getInitializer();
@@ -1189,22 +1325,31 @@ void NVPTXAsmPrinter::printModuleLevelGV(const GlobalVariable *GVar,
                     "' requires at least PTX ISA version 7.1");
               O << " .u8 ";
               getSymbol(GVar)->print(O, MAI);
-              O << "[" << ElementSize << "] = {";
-              aggBuffer.printBytes(O);
-              O << "}";
+              O << "[" << ElementSize << "]";
+              if (EmitInitializer) {
+                O << " = {";
+                aggBuffer.printBytes(O);
+                O << "}";
+              }
             } else {
               O << " .u" << ptrSize * 8 << " ";
               getSymbol(GVar)->print(O, MAI);
-              O << "[" << ElementSize / ptrSize << "] = {";
-              aggBuffer.printWords(O);
-              O << "}";
+              O << "[" << ElementSize / ptrSize << "]";
+              if (EmitInitializer) {
+                O << " = {";
+                aggBuffer.printWords(O);
+                O << "}";
+              }
             }
           } else {
             O << " .b8 ";
             getSymbol(GVar)->print(O, MAI);
-            O << "[" << ElementSize << "] = {";
-            aggBuffer.printBytes(O);
-            O << "}";
+            O << "[" << ElementSize << "]";
+            if (EmitInitializer) {
+              O << " = {";
+              aggBuffer.printBytes(O);
+              O << "}";
+            }
           }
         } else {
           O << " .b8 ";
@@ -1224,7 +1369,6 @@ void NVPTXAsmPrinter::printModuleLevelGV(const GlobalVariable *GVar,
       llvm_unreachable("type not supported yet");
     }
   }
-  O << ";\n";
 }
 
 void NVPTXAsmPrinter::AggBuffer::printSymbol(unsigned nSym, raw_ostream &os) {
diff --git a/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.h b/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.h
index 8a9b811fec200..cf8830efd9733 100644
--- a/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.h
+++ b/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.h
@@ -229,6 +229,10 @@ class LLVM_LIBRARY_VISIBILITY NVPTXAsmPrinter : public AsmPrinter {
 
   void emitPTXGlobalVariable(const GlobalVariable *GVar, raw_ostream &O,
                              const NVPTXSubtarget &STI);
+  void emitPTXGlobalVariableDefinition(const GlobalVariable *GVar,
+                                       raw_ostream &O,
+                                       const NVPTXSubtarget &STI,
+                                       bool EmitInitializer);
   void emitPTXAddressSpace(unsigned int AddressSpace, raw_ostream &O) const;
   std::string getPTXFundamentalTypeStr(Type *Ty, bool = true) const;
   void printScalarConstant(const Constant *CPV, raw_ostream &O);
diff --git a/llvm/test/CodeGen/NVPTX/global-cycle-internal.ll b/llvm/test/CodeGen/NVPTX/global-cycle-internal.ll
new file mode 100644
index 0000000000000..6aaa8dabf0842
--- /dev/null
+++ b/llvm/test/CodeGen/NVPTX/global-cycle-internal.ll
@@ -0,0 +1,10 @@
+; RUN: not --crash llc < %s -mtriple=nvptx64 -mcpu=sm_20 2>&1 | FileCheck %s
+
+; A PTX .extern declaration cannot be resolved by a static definition, so an
+; internal-only cycle cannot be emitted for ptxas versions that reject forward
+; references in initializers.
+
+; CHECK: LLVM ERROR: Circular dependency found in global variable set
+
+ at a = internal addrspace(1) global ptr addrspace(1) @b
+ at b = internal addrspace(1) global ptr addrspace(1) @a
diff --git a/llvm/test/CodeGen/NVPTX/global-cycle.ll b/llvm/test/CodeGen/NVPTX/global-cycle.ll
new file mode 100644
index 0000000000000..4a4ad4c4f630a
--- /dev/null
+++ b/llvm/test/CodeGen/NVPTX/global-cycle.ll
@@ -0,0 +1,48 @@
+; RUN: llc < %s -mtriple=nvptx -mcpu=sm_20 | FileCheck %s --check-prefix=PTX32
+; RUN: llc < %s -mtriple=nvptx64 -mcpu=sm_20 | FileCheck %s --check-prefix=PTX64
+; RUN: %if ptxas-ptr32 %{ llc < %s -mtriple=nvptx -mcpu=sm_20 | %ptxas-verify %}
+; RUN: %if ptxas %{ llc < %s -mtriple=nvptx64 -mcpu=sm_20 | %ptxas-verify %}
+
+; PTX can represent cyclic global references as relocations when ptxas has
+; already seen a compatible declaration for the referenced symbol.
+; Consumer appears first in IR to also check dependency-first SCC ordering.
+
+; PTX32:      .extern .global .align 4 .u32 a;
+; PTX32-NEXT: .extern .global .align 4 .u32 b;
+; PTX32-NEXT: .visible .global .align 4 .u32 a = b;
+; PTX32-NEXT: .visible .global .align 4 .u32 b = a;
+; PTX32-NEXT: .visible .global .align 4 .u32 consumer = a;
+; PTX64:      .extern .global .align 8 .u64 a;
+; PTX64-NEXT: .extern .global .align 8 .u64 b;
+; PTX64-NEXT: .visible .global .align 8 .u64 a = b;
+; PTX64-NEXT: .visible .global .align 8 .u64 b = a;
+; PTX64-NEXT: .visible .global .align 8 .u64 consumer = a;
+ at consumer = addrspace(1) global ptr addrspace(1) @a
+ at a = addrspace(1) global ptr addrspace(1) @b
+ at b = addrspace(1) global ptr addrspace(1) @a
+
+; If a cycle includes an internal global, only the externally-visible global is
+; forward-declared. The definitions are still ordered so the internal symbol is
+; defined before it is referenced.
+
+; PTX32:      .extern .global .align 4 .u32 c;
+; PTX32-NEXT: .global .align 4 .u32 d = c;
+; PTX32-NEXT: .visible .global .align 4 .u32 c = d;
+; PTX64:      .extern .global .align 8 .u64 c;
+; PTX64-NEXT: .global .align 8 .u64 d = c;
+; PTX64-NEXT: .visible .global .align 8 .u64 c = d;
+ at c = addrspace(1) global ptr addrspace(1) @d
+ at d = internal addrspace(1) global ptr addrspace(1) @c
+
+; Non-local weak definitions can also resolve compatible .extern declarations.
+
+; PTX32:      .extern .global .align 4 .u32 weak_a;
+; PTX32-NEXT: .extern .global .align 4 .u32 weak_b;
+; PTX32-NEXT: .weak .global .align 4 .u32 weak_a = weak_b;
+; PTX32-NEXT: .weak .global .align 4 .u32 weak_b = weak_a;
+; PTX64:      .extern .global .align 8 .u64 weak_a;
+; PTX64-NEXT: .extern .global .align 8 .u64 weak_b;
+; PTX64-NEXT: .weak .global .align 8 .u64 weak_a = weak_b;
+; PTX64-NEXT: .weak .global .align 8 .u64 weak_b = weak_a;
+ at weak_a = weak addrspace(1) global ptr addrspace(1) @weak_b
+ at weak_b = weak addrspace(1) global ptr addrspace(1) @weak_a
diff --git a/llvm/test/CodeGen/NVPTX/global-ordering.ll b/llvm/test/CodeGen/NVPTX/global-ordering.ll
index 8fb0024bbc092..b65279212ec58 100644
--- a/llvm/test/CodeGen/NVPTX/global-ordering.ll
+++ b/llvm/test/CodeGen/NVPTX/global-ordering.ll
@@ -28,3 +28,10 @@
 
 ; PTX64: .visible .global .align 8 .u64 sadd[2] = {g+4, 7};
 @sadd = addrspace(1) global { i64, i64 } { i64 add (i64 ptrtoint (ptr addrspace(1) @g to i64), i64 4), i64 7 }
+
+; Self-references are emitted as a declaration followed by the definition.
+; PTX32:      .extern .global .align 4 .u32 self;
+; PTX32-NEXT: .visible .global .align 4 .u32 self = self;
+; PTX64:      .extern .global .align 8 .u64 self;
+; PTX64-NEXT: .visible .global .align 8 .u64 self = self;
+ at self = addrspace(1) global ptr addrspace(1) @self
diff --git a/llvm/test/CodeGen/NVPTX/packed-aggr-self-ptx70.ll b/llvm/test/CodeGen/NVPTX/packed-aggr-self-ptx70.ll
new file mode 100644
index 0000000000000..06e6222583995
--- /dev/null
+++ b/llvm/test/CodeGen/NVPTX/packed-aggr-self-ptx70.ll
@@ -0,0 +1,11 @@
+; RUN: not --crash llc < %s -mtriple=nvptx -mcpu=sm_20 -mattr=+ptx70 2>&1 | FileCheck %s
+
+; Self-referential packed aggregates still require mask(), which is available
+; only in PTX ISA 7.1 and later.
+
+; CHECK: LLVM ERROR: initialized packed aggregate with pointers 'self_packed' requires at least PTX ISA version 7.1
+
+%t = type <{ ptr, i8 }>
+ at self_packed = addrspace(1) global %t <{
+  ptr addrspacecast (ptr addrspace(1) getelementptr (i8, ptr addrspace(1) @self_packed, i32 3) to ptr),
+  i8 7 }>, align 1
diff --git a/llvm/test/CodeGen/NVPTX/packed-aggr.ll b/llvm/test/CodeGen/NVPTX/packed-aggr.ll
index 353f1cba74eb0..2065b1a35f326 100644
--- a/llvm/test/CodeGen/NVPTX/packed-aggr.ll
+++ b/llvm/test/CodeGen/NVPTX/packed-aggr.ll
@@ -93,3 +93,18 @@ declare void @func()
 ; CHECK64-SAME: 0xFF(func), 0xFF00(func), 0xFF0000(func), 0xFF000000(func),
 ; CHECK64-SAME: 0xFF00000000(func), 0xFF0000000000(func), 0xFF000000000000(func), 0xFF00000000000000(func),
 ; CHECK64-SAME: 9, 0};
+
+;; Test that self-referential packed aggregates also use masked relocations
+;; when the aggregate size is not a multiple of the pointer size.
+
+%t6 = type <{ ptr, i8 }>
+ at self_packed = addrspace(1) global %t6 <{
+; CHECK32:      .extern .global .align 1 .u8 self_packed[5];
+; CHECK32-NEXT: .visible .global .align 1 .u8 self_packed[5] = {
+; CHECK32-SAME: 0xFF(generic(self_packed)+3), 0xFF00(generic(self_packed)+3), 0xFF0000(generic(self_packed)+3), 0xFF000000(generic(self_packed)+3), 7};
+; CHECK64:      .extern .global .align 1 .u8 self_packed[9];
+; CHECK64-NEXT: .visible .global .align 1 .u8 self_packed[9] = {
+; CHECK64-SAME: 0xFF(generic(self_packed)+3), 0xFF00(generic(self_packed)+3), 0xFF0000(generic(self_packed)+3), 0xFF000000(generic(self_packed)+3),
+; CHECK64-SAME: 0xFF00000000(generic(self_packed)+3), 0xFF0000000000(generic(self_packed)+3), 0xFF000000000000(generic(self_packed)+3), 0xFF00000000000000(generic(self_packed)+3), 7};
+  ptr addrspacecast (ptr addrspace(1) getelementptr (i8, ptr addrspace(1) @self_packed, i32 3) to ptr),
+  i8 7 }>, align 1

>From ee557790d6785d08cbaf0a561ab89e8df9298ca2 Mon Sep 17 00:00:00 2001
From: aryanmagoon <amagoon at nvidia.com>
Date: Thu, 23 Jul 2026 14:19:11 +0000
Subject: [PATCH 2/2] Refine cyclic global initializer dependencies

---
 llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp     | 29 +++++++++++------
 llvm/test/CodeGen/NVPTX/global-cycle-alias.ll | 26 ++++++++++++++++
 .../NVPTX/global-cycle-internal-subcycle.ll   | 14 +++++++++
 llvm/test/CodeGen/NVPTX/global-cycle.ll       | 31 +++++++++++++++++++
 llvm/test/CodeGen/NVPTX/global-ordering.ll    | 28 +++++++++++++++++
 5 files changed, 119 insertions(+), 9 deletions(-)
 create mode 100644 llvm/test/CodeGen/NVPTX/global-cycle-alias.ll
 create mode 100644 llvm/test/CodeGen/NVPTX/global-cycle-internal-subcycle.ll

diff --git a/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp b/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp
index b120850f7da85..9e83b368e27d4 100644
--- a/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp
+++ b/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp
@@ -33,9 +33,9 @@
 #include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/DenseSet.h"
 #include "llvm/ADT/SCCIterator.h"
-#include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/Sequence.h"
+#include "llvm/ADT/SmallPtrSet.h"
 #include "llvm/ADT/SmallString.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/StringExtras.h"
@@ -145,15 +145,28 @@ static void emitInitialRawDwarfLocDirective(const MachineFunction &MF,
 namespace {
 
 /// Return a list of GlobalVariables on which \p V depends.
-static void discoverDependentGlobals(
-    const Value *V, SmallVectorImpl<const GlobalVariable *> &Globals,
-    SmallPtrSetImpl<const GlobalVariable *> &Seen) {
+static void
+discoverDependentGlobals(const Value *V,
+                         SmallVectorImpl<const GlobalVariable *> &Globals,
+                         SmallPtrSetImpl<const GlobalVariable *> &Seen) {
   if (const GlobalVariable *GV = dyn_cast<GlobalVariable>(V)) {
     if (Seen.insert(GV).second)
       Globals.push_back(GV);
     return;
   }
 
+  // Global values are emitted as symbols. Their operands do not contribute to
+  // the initializer expression that refers to that symbol.
+  if (isa<GlobalValue>(V))
+    return;
+
+  // lowerConstantForGV emits a GEP as its base symbol plus a constant byte
+  // offset. Symbols used to compute an index are not part of that expression.
+  if (const GEPOperator *GEP = dyn_cast<GEPOperator>(V)) {
+    discoverDependentGlobals(GEP->getPointerOperand(), Globals, Seen);
+    return;
+  }
+
   if (const User *U = dyn_cast<User>(V))
     for (const auto &O : U->operands())
       discoverDependentGlobals(O, Globals, Seen);
@@ -221,8 +234,7 @@ using GlobalVariableSCCIterator =
 static bool shouldSkipModuleLevelGlobal(const GlobalVariable &GV) {
   if (GV.hasSection() && GV.getSection() == "llvm.metadata")
     return true;
-  return GV.getName().starts_with("llvm.") ||
-         GV.getName().starts_with("nvvm.");
+  return GV.getName().starts_with("llvm.") || GV.getName().starts_with("nvvm.");
 }
 
 static bool isForwardDeclarableGlobal(const GlobalVariable *GVar) {
@@ -245,8 +257,7 @@ static bool isForwardDeclarableGlobal(const GlobalVariable *GVar) {
 /// Order definitions after treating references to forward-declared globals as
 /// already satisfied. A remaining cycle cannot be emitted portably because it
 /// requires an undeclared forward reference.
-static SmallVector<const GlobalVariable *, 4>
-orderDefinitionsInSCC(
+static SmallVector<const GlobalVariable *, 4> orderDefinitionsInSCC(
     ArrayRef<const GlobalVariableDependencyNode *> SCC,
     const DenseSet<const GlobalVariableDependencyNode *> &ForwardDeclared) {
   using Node = GlobalVariableDependencyNode;
@@ -964,7 +975,7 @@ void NVPTXAsmPrinter::emitGlobals(const Module &M) {
            GlobalVariableSCCIterator::begin(DependencyGraph.getEntryNode());
        !I.isAtEnd(); ++I) {
     SmallVector<const GlobalVariableDependencyNode *, 4> SCC(I->begin(),
-                                                              I->end());
+                                                             I->end());
 
     // Nothing points to the synthetic root, so it is always in its own SCC.
     if (!SCC.front()->GV) {
diff --git a/llvm/test/CodeGen/NVPTX/global-cycle-alias.ll b/llvm/test/CodeGen/NVPTX/global-cycle-alias.ll
new file mode 100644
index 0000000000000..05f4fa97072b5
--- /dev/null
+++ b/llvm/test/CodeGen/NVPTX/global-cycle-alias.ll
@@ -0,0 +1,26 @@
+; RUN: llc < %s -mtriple=nvptx64 -mcpu=sm_30 -mattr=+ptx64 | FileCheck %s
+; RUN: %if ptxas-isa-6.4 %{ llc < %s -mtriple=nvptx64 -mcpu=sm_30 -mattr=+ptx64 | %ptxas-verify %}
+
+; NVPTX aliases must ultimately refer to functions. Indexing through a
+; zero-sized type preserves the aliasee function's address while making
+; @alias_cycle_b a nested operand of the GlobalAlias. The PTX initializer for
+; @alias_cycle_a refers only to the alias symbol, so the alias's operands must
+; not create the false dependency cycle
+; @alias_cycle_a -> @cycle_alias -> @alias_cycle_b -> @alias_cycle_a.
+
+; CHECK-NOT:  .extern .global
+; CHECK:      .global .align 8 .u64 alias_cycle_a = cycle_alias;
+; CHECK-NEXT: .global .align 8 .u64 alias_cycle_b = alias_cycle_a;
+; CHECK-NOT:  .extern .global
+; CHECK:      .alias cycle_alias, alias_target;
+
+%empty = type {}
+
+ at alias_cycle_b = internal addrspace(1) global ptr addrspace(1) @alias_cycle_a
+ at alias_cycle_a = internal addrspace(1) global i64 ptrtoint (ptr @cycle_alias to i64)
+ at cycle_alias = alias %empty, getelementptr (%empty, ptr @alias_target,
+    i64 ptrtoint (ptr addrspace(1) @alias_cycle_b to i64))
+
+define void @alias_target() {
+  ret void
+}
diff --git a/llvm/test/CodeGen/NVPTX/global-cycle-internal-subcycle.ll b/llvm/test/CodeGen/NVPTX/global-cycle-internal-subcycle.ll
new file mode 100644
index 0000000000000..7af6ca9f2f9c1
--- /dev/null
+++ b/llvm/test/CodeGen/NVPTX/global-cycle-internal-subcycle.ll
@@ -0,0 +1,14 @@
+; RUN: not --crash llc < %s -mtriple=nvptx64 -mcpu=sm_20 2>&1 | FileCheck %s
+
+; A cyclic SCC is not necessarily made valid by the presence of a
+; forward-declarable global. Declaring @outer breaks the larger loop, but the
+; internal-only @inner_a <-> @inner_b subcycle still cannot be ordered.
+
+; CHECK: LLVM ERROR: Circular dependency found in global variable set
+
+ at outer = addrspace(1) global ptr addrspace(1) @inner_a
+ at inner_a = internal addrspace(1) global { ptr addrspace(1), ptr addrspace(1) } {
+  ptr addrspace(1) @outer,
+  ptr addrspace(1) @inner_b
+}
+ at inner_b = internal addrspace(1) global ptr addrspace(1) @inner_a
diff --git a/llvm/test/CodeGen/NVPTX/global-cycle.ll b/llvm/test/CodeGen/NVPTX/global-cycle.ll
index 4a4ad4c4f630a..6203c6c4f81f0 100644
--- a/llvm/test/CodeGen/NVPTX/global-cycle.ll
+++ b/llvm/test/CodeGen/NVPTX/global-cycle.ll
@@ -6,6 +6,8 @@
 ; PTX can represent cyclic global references as relocations when ptxas has
 ; already seen a compatible declaration for the referenced symbol.
 ; Consumer appears first in IR to also check dependency-first SCC ordering.
+; The cycles below are mutually disconnected, so emitting all of them also
+; checks that the synthetic root reaches distinct cyclic SCCs.
 
 ; PTX32:      .extern .global .align 4 .u32 a;
 ; PTX32-NEXT: .extern .global .align 4 .u32 b;
@@ -34,6 +36,22 @@
 @c = addrspace(1) global ptr addrspace(1) @d
 @d = internal addrspace(1) global ptr addrspace(1) @c
 
+; One forward declaration can break a larger cycle containing multiple
+; internal globals. This requires multiple ready-list updates to order the
+; definitions as three_c, three_b, three_a.
+
+; PTX32:      .extern .global .align 4 .u32 three_a;
+; PTX32-NEXT: .global .align 4 .u32 three_c = three_a;
+; PTX32-NEXT: .global .align 4 .u32 three_b = three_c;
+; PTX32-NEXT: .visible .global .align 4 .u32 three_a = three_b;
+; PTX64:      .extern .global .align 8 .u64 three_a;
+; PTX64-NEXT: .global .align 8 .u64 three_c = three_a;
+; PTX64-NEXT: .global .align 8 .u64 three_b = three_c;
+; PTX64-NEXT: .visible .global .align 8 .u64 three_a = three_b;
+ at three_a = addrspace(1) global ptr addrspace(1) @three_b
+ at three_b = internal addrspace(1) global ptr addrspace(1) @three_c
+ at three_c = internal addrspace(1) global ptr addrspace(1) @three_a
+
 ; Non-local weak definitions can also resolve compatible .extern declarations.
 
 ; PTX32:      .extern .global .align 4 .u32 weak_a;
@@ -46,3 +64,16 @@
 ; PTX64-NEXT: .weak .global .align 8 .u64 weak_b = weak_a;
 @weak_a = weak addrspace(1) global ptr addrspace(1) @weak_b
 @weak_b = weak addrspace(1) global ptr addrspace(1) @weak_a
+
+; Forward declarations retain the globals' PTX state space.
+
+; PTX32:      .extern .const .align 4 .u32 const_a;
+; PTX32-NEXT: .extern .const .align 4 .u32 const_b;
+; PTX32-NEXT: .visible .const .align 4 .u32 const_a = const_b;
+; PTX32-NEXT: .visible .const .align 4 .u32 const_b = const_a;
+; PTX64:      .extern .const .align 8 .u64 const_a;
+; PTX64-NEXT: .extern .const .align 8 .u64 const_b;
+; PTX64-NEXT: .visible .const .align 8 .u64 const_a = const_b;
+; PTX64-NEXT: .visible .const .align 8 .u64 const_b = const_a;
+ at const_a = addrspace(4) constant ptr addrspace(4) @const_b
+ at const_b = addrspace(4) constant ptr addrspace(4) @const_a
diff --git a/llvm/test/CodeGen/NVPTX/global-ordering.ll b/llvm/test/CodeGen/NVPTX/global-ordering.ll
index b65279212ec58..bfa01bf054ee6 100644
--- a/llvm/test/CodeGen/NVPTX/global-ordering.ll
+++ b/llvm/test/CodeGen/NVPTX/global-ordering.ll
@@ -22,6 +22,29 @@
 @b = addrspace(1) global i8 1
 
 
+; A GEP initializer is emitted as its base plus a constant byte offset. A
+; symbol nested in an index expression is not emitted and must not create a
+; false self-cycle. The zero-sized element makes the computed offset zero.
+; Consumer appears first in IR to check dependency-first emission.
+;
+; PTX32:      .global .align 4 .u32 gep_index_base = 7;
+; PTX32-NEXT: .global .align 4 .u32 gep_index = gep_index_base;
+; PTX64:      .global .align 4 .u32 gep_index_base = 7;
+; PTX64-NEXT: .global .align 8 .u64 gep_index = gep_index_base;
+%empty = type {}
+ at gep_index = internal addrspace(1) global ptr addrspace(1) getelementptr (
+    %empty, ptr addrspace(1) @gep_index_base,
+    i64 ptrtoint (ptr addrspace(1) @gep_index to i64))
+ at gep_index_base = internal addrspace(1) global i32 7
+
+
+; A Function is also referenced as a single symbol. Its personality is not
+; part of the global initializer and must not create a false self-cycle.
+; PTX32: .global .align 4 .u32 function_leaf_ref = function_leaf;
+; PTX64: .global .align 8 .u64 function_leaf_ref = function_leaf;
+ at function_leaf_ref = internal addrspace(1) global ptr @function_leaf
+
+
 ; Emit a global aggregate with a field computed from the address of another
 ; global.
 @g = addrspace(1) global i8 0
@@ -35,3 +58,8 @@
 ; PTX64:      .extern .global .align 8 .u64 self;
 ; PTX64-NEXT: .visible .global .align 8 .u64 self = self;
 @self = addrspace(1) global ptr addrspace(1) @self
+
+define internal void @function_leaf()
+    personality ptr addrspace(1) @function_leaf_ref {
+  ret void
+}



More information about the llvm-commits mailing list