[clang] [llvm] [ThinLTO][Split] Split module for parallel compilation in backend (1/N) (PR #198702)

via llvm-commits llvm-commits at lists.llvm.org
Mon Sep 28 05:18:55 PDT 2026


https://github.com/mmjjpp updated https://github.com/llvm/llvm-project/pull/198702

>From 1ca0407b33fcd64dd08a3ec243f09f8bb7e7ae6a Mon Sep 17 00:00:00 2001
From: maojiaping <maojiaping1 at huawei.com>
Date: Wed, 20 May 2026 11:22:30 +0800
Subject: [PATCH 01/13] [ThinLTO][Split] Split module for parallel compilation
 in backend

An interface for splitting a module by callgraph is added. This
interface is called in the thinlto backend phase. The module is
split into N Mparts, and opt and codegen are performed on the
Mparts in parallel to implement parallel compilation in the
thinlto backend.
---
 .../llvm/Transforms/Utils/SplitModuleCG.h     |  34 ++
 llvm/lib/LTO/LTOBackend.cpp                   | 292 +++++++++++++++++-
 llvm/lib/Transforms/Utils/CMakeLists.txt      |   1 +
 llvm/lib/Transforms/Utils/SplitModuleCG.cpp   |  26 ++
 4 files changed, 336 insertions(+), 17 deletions(-)
 create mode 100644 llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
 create mode 100644 llvm/lib/Transforms/Utils/SplitModuleCG.cpp

diff --git a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
new file mode 100644
index 0000000000000..e60c4e931d40c
--- /dev/null
+++ b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
@@ -0,0 +1,34 @@
+#ifndef LLVM_TRANSFORMS_UTILS_SPLITMODULECG_H
+#define LLVM_TRANSFORMS_UTILS_SPLITMODULECG_H
+
+#include "llvm/Analysis/CallGraph.h"
+#include "llvm/Analysis/ModuleSummaryAnalysis.h"
+#include "llvm/LTO/Config.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/DenseSet.h"
+
+namespace llvm {
+/// Splits the module M into N linkable partitions. The function ModuleCallback
+/// is called N times passing each individual partition as the MPart argument.
+class SplitModuleCG {
+public:
+  using ModuleCreationCallback =
+      function_ref<void(std::unique_ptr<Module> MPart, unsigned PartitionId)>;
+  SplitModuleCG(Module &M,
+                const ModuleSummaryIndex &CombinedIndex,
+                unsigned LimitPartition = 0);
+  void SplitModule(ModuleCreationCallback ModuleCallback,
+                   const llvm::lto::Config &C);
+
+  unsigned getPartitionNum() { return N; }
+
+  private:
+  unsigned N;
+  Module &M;
+  CallGraph CG;
+  DenseSet<const Function *> EntryFuncs;
+};
+
+} // end namespace llvm
+
+#endif // LLVM_TRANSFORMS_UTILS_SPLITMODULECG_H
diff --git a/llvm/lib/LTO/LTOBackend.cpp b/llvm/lib/LTO/LTOBackend.cpp
index 11c9b2f61988b..10d66a1a6097b 100644
--- a/llvm/lib/LTO/LTOBackend.cpp
+++ b/llvm/lib/LTO/LTOBackend.cpp
@@ -34,8 +34,10 @@
 #include "llvm/Plugins/PassPlugin.h"
 #include "llvm/Support/Error.h"
 #include "llvm/Support/FileSystem.h"
+#include "llvm/Support/FileUtilities.h"
 #include "llvm/Support/MemoryBuffer.h"
 #include "llvm/Support/Path.h"
+#include "llvm/Support/Program.h"
 #include "llvm/Support/ThreadPool.h"
 #include "llvm/Support/ToolOutputFile.h"
 #include "llvm/Support/VirtualFileSystem.h"
@@ -45,6 +47,8 @@
 #include "llvm/Transforms/IPO/WholeProgramDevirt.h"
 #include "llvm/Transforms/Utils/FunctionImportUtils.h"
 #include "llvm/Transforms/Utils/SplitModule.h"
+#include "llvm/Transforms/Utils/SplitModuleCG.h"
+#include <filesystem>
 #include <optional>
 
 using namespace llvm;
@@ -80,6 +84,23 @@ static cl::list<std::string>
                              "path matches this for -save-temps options"),
                     cl::CommaSeparated, cl::Hidden);
 
+static cl::opt<unsigned> ThinLTOSplitModuleSizeThreshold(
+    "thinlto-split-module-size-threshold", cl::Hidden, cl::init(500),
+    cl::desc("Control the amount of whether split in thinlto backend"
+             "accroding to the size of a module."));
+
+static cl::opt<float> ThinLTOSplitModuleSizeRateThreshold(
+    "thinlto-split-module-size-rate-threshold", cl::Hidden, cl::init(0.5),
+    cl::desc("Whether to split in thinlto backend based on the ratio of "
+             "(callgraph size)/(module size)"));
+
+static cl::opt<unsigned> ThinLTOSplitPartitions(
+    "thinlto-split-partitions", cl::Hidden, cl::init(0),
+    cl::desc("Control split to how many partitions in thinlto backend."));
+
+static cl::opt<bool> ThinLTOSplit("thinlto-split", cl::init(false),
+			   cl::desc("Enable split module in thinlto backend."));
+
 namespace llvm {
 extern cl::opt<bool> NoPGOWarnMismatch;
 }
@@ -124,12 +145,19 @@ Error Config::addSaveTemps(std::string OutputFileName, bool UseInputModulePath,
       if (LinkerHook && !LinkerHook(Task, M))
         return false;
 
+      auto extract_filename = [](const std::string &path) -> std::string {
+        std::filesystem::path fs_path(path);
+        return fs_path.filename().string();
+      };
+
       std::string PathPrefix;
       // If this is the combined module (not a ThinLTO backend compile) or the
       // user hasn't requested using the input module's path, emit to a file
       // named from the provided OutputFileName with the Task ID appended.
       if (M.getModuleIdentifier() == "ld-temp.o" || !UseInputModulePath) {
         PathPrefix = OutputFileName;
+        if (ThinLTOSplit)
+          PathPrefix += extract_filename(M.getSourceFileName()) + ".";
         if (Task != (unsigned)-1)
           PathPrefix += utostr(Task) + ".";
       } else
@@ -512,6 +540,212 @@ static void codegen(const Config &Conf, TargetMachine *TM,
     report_fatal_error(std::move(Err));
 }
 
+static unsigned calFunctionSize(const llvm::Function &F) {
+  unsigned size = 0;
+  for (const auto &BB : F)
+    size += std::distance(BB.begin(), BB.end());
+  return size;
+}
+
+static unsigned calModuleSize(const llvm::Module &M) {
+  unsigned size = 0;
+  for (const auto &F : M)
+    size += calFunctionSize(F);
+  return size;
+}
+
+static bool canDoSplitModule(const llvm::Module &M) {
+  if (calModuleSize(M) < ThinLTOSplitModuleSizeThreshold)
+    return false;
+  return true;
+}
+
+static bool HasLargeCG(Module &Mod, const ModuleSummaryIndex &CombinedIndex) {
+  // TODO: Check whether there has large callgraphs. When multiple callgraphs
+  // are split, thinlto parallel compilation can bring benefits.
+  return true;
+}
+
+struct TaskIdAllocator {
+  using TaskId = unsigned;
+
+  // Use the most significant bit (MSB) as a namespace tag.
+  // - Original ThinLTO backend tasks are expected to have MSB == 0.
+  // - Split partitions allocated by this allocator always have MSB == 1.
+  // This guarantees the two ID spaces never overlap.
+  static constexpr TaskId tag() {
+    return TaskId{1} << (std::numeric_limits<TaskId>::digits - 1);
+  }
+
+  // Monotonic sequence counter for split partitions (MSB must remain 0 here).
+  std::atomic<TaskId> seq{0};
+
+  // Allocate a globally unique TaskId for a split partition.
+  // The returned ID is `tag() | seq`, so it lives in the MSB==1 namespace.
+  TaskId alloc() {
+    TaskId v = seq.fetch_add(1, std::memory_order_relaxed);
+
+    // If the counter ever reaches the MSB, we'd overlap namespaces.
+    // This indicates an overflow / too many partitions.
+    if (v & tag())
+      report_fatal_error("Partition TaskId overflow: seq reached the tag bit.");
+
+    return tag() | v;
+  }
+
+  // Helper for sanity checks / debugging.
+  static bool isPartition(TaskId id) { return (id & tag()) != 0; }
+};
+
+// Global allocator shared by all split partitions.
+static TaskIdAllocator gSplitTaskIds;
+
+static bool splitOptAndCodeGenThin(unsigned task, const Config &C,
+                                   TargetMachine *TM, AddStreamFn AddStream,
+                                   unsigned ParallelCodeGenParallelismLevel,
+                                   Module &Mod,
+                                   const ModuleSummaryIndex &CombinedIndex,
+                                   const std::vector<uint8_t> &CmdArgs,
+                                   bool DoOpt, AddStreamFn IRAddStream,
+                                   ArrayRef<StringRef> &BitcodeLibFuncs) {
+  unsigned ThreadCount = 0;
+  const Target *T = &TM->getTarget();
+
+  static std::mutex PrintMutex;
+
+  SplitModuleCG SplitModuleCG(Mod, CombinedIndex, ParallelCodeGenParallelismLevel);
+  ParallelCodeGenParallelismLevel = SplitModuleCG.getPartitionNum();
+
+  std::vector<std::string> TempObjectFiles(ParallelCodeGenParallelismLevel);
+  std::vector<llvm::FileRemover> TempFileRemovers(ParallelCodeGenParallelismLevel);
+
+  const auto HandleModulePartition = [&](std::unique_ptr<Module> MPart,
+                                         unsigned PartitionId) {
+    unsigned CurrentThreadId, UniqueTaskId;
+    {
+      std::lock_guard<std::mutex> Lock(PrintMutex);
+      CurrentThreadId = ThreadCount++;
+
+      // In distributed ThinLTO, `task` may be a sentinel (e.g. -1 cast to
+      // unsigned), which becomes UINT_MAX and naturally has MSB==1. Treat it
+      // as "no base task id" and don't enforce the namespace check on it.
+      //
+      // We do not rely on the incoming `task` for partition uniqueness: split
+      // partitions get a dedicated UniqueTaskId allocated below.
+      if (task != std::numeric_limits<unsigned>::max()) {
+        assert(!TaskIdAllocator::isPartition(task) &&
+               "Original ThinLTO TaskId unexpectedly overlaps the partition "
+               "namespace");
+      }
+      UniqueTaskId = gSplitTaskIds.alloc();
+    }
+
+    std::unique_ptr<TargetMachine> ThreadTM = createTargetMachine(C, T, *MPart);
+
+    if (DoOpt) {
+      if (!opt(C, ThreadTM.get(), UniqueTaskId, *MPart, /*IsThinLTO=*/true,
+               /*ExportSummary=*/nullptr, /*ImportSummary=*/&CombinedIndex,
+               CmdArgs, BitcodeLibFuncs)) {
+        report_fatal_error("Failed to gen opt for split mod in thread.");
+      }
+
+      // Save the current module before the first codegen round.
+      // Note that the second codegen round runs only `codegen()` without
+      // running `opt()`. We're not reaching here as it's bailed out earlier
+      // with `CodeGenOnly` which has been set in `SecondRoundThinBackend`.
+      if (IRAddStream)
+        cgdata::saveModuleForTwoRounds(*MPart, task + CurrentThreadId,
+                                       IRAddStream);
+    }
+
+    auto splitStream = [&](unsigned task, const Twine &moduleName)
+        -> Expected<std::unique_ptr<CachedFileStream>> {
+      int FD;
+      SmallString<128> TempFilename;
+      if (std::error_code EC = sys::fs::createTemporaryFile(
+              "thinlto-split", "o", FD, TempFilename))
+        return errorCodeToError(EC);
+
+      TempObjectFiles[PartitionId] = std::string(TempFilename.str());
+      TempFileRemovers[PartitionId].setFile(TempObjectFiles[PartitionId]);
+
+      auto OS = std::make_unique<raw_fd_ostream>(
+          FD, true, /*CloseOnDestruct*/true);
+
+      auto Stream = std::make_unique<CachedFileStream>(
+          std::move(OS), std::string(TempFilename.str()));
+
+      return std::move(Stream);
+    };
+
+    codegen(C, ThreadTM.get(), splitStream, UniqueTaskId, *MPart,
+            CombinedIndex);
+  };
+
+  SplitModuleCG.SplitModule(HandleModulePartition, C);
+
+  // Use ld.lld to combine the partitions into a object.
+  if (TempObjectFiles.empty()) {
+    llvm::errs() << "TempObjectFiles.empty()\n";
+    return true;
+  }
+
+  auto FinalStream = AddStream(task, Mod.getModuleIdentifier());
+  if (!FinalStream)
+    report_fatal_error("Failed to open final output stream");
+
+  int MergedFD;
+  SmallString<128> MergedFilename;
+  if (sys::fs::createTemporaryFile("thinlto-merged", "o", MergedFD,
+                                   MergedFilename))
+    report_fatal_error("Failed to create merged temp file.");
+  llvm::FileRemover MergedFileRemover(MergedFilename);
+  sys::fs::closeFile(MergedFD);
+
+  std::vector<StringRef> Args;
+  std::string LinkerPath = "";
+  if (auto Path = sys::findProgramByName("ld.lld"))
+    LinkerPath = *Path;
+  else if (auto Path = sys::findProgramByName("ld"))
+    LinkerPath = *Path;
+
+  if (LinkerPath.empty())
+    report_fatal_error("Cannot find linkeer (ld or ld.lld) to merge partitions.");
+
+  Args.push_back(LinkerPath);
+  Args.push_back("-r");
+  Args.push_back("-o");
+  Args.push_back(MergedFilename);
+
+  for (const auto &File : TempObjectFiles)
+    Args.push_back(File);
+
+  std::string ErrMsg;
+  int Result = sys::ExecuteAndWait(LinkerPath, Args, /*Env=*/std::nullopt,
+                                   /*Redirects=*/{}, /*SecondsToWait=*/0,
+                                   /*MemoryLimit=*/0, &ErrMsg);
+
+  if (Result != 0) {
+    errs() << "Linker failed: " << ErrMsg << "\n";
+    report_fatal_error("Failed to merge split objects.");
+  }
+
+  {
+    std::unique_ptr<CachedFileStream> &FinalFileStream = *FinalStream;
+    auto BufferOrErr = MemoryBuffer::getFile(MergedFilename);
+    if (!BufferOrErr)
+      report_fatal_error("Failed to read merged object.");
+
+    FinalFileStream->OS->write(BufferOrErr.get()->getBufferStart(),
+                               BufferOrErr.get()->getBufferSize());
+    if (Error Err = FinalFileStream->commit()) {
+      report_fatal_error(Twine("Failed to commit final file stream: ") +
+                         toString(std::move(Err)));
+    }
+  }
+  return true;
+}
+
 static void splitCodeGen(const Config &C, TargetMachine *TM,
                          AddStreamFn AddStream,
                          unsigned ParallelCodeGenParallelismLevel, Module &Mod,
@@ -676,11 +910,28 @@ Error lto::thinBackend(const Config &Conf, unsigned Task, AddStreamFn AddStream,
   // the module, if applicable.
   Mod.setPartialSampleProfileRatio(CombinedIndex);
 
+  bool ProfitableToSplit = true;
+  if (ThinLTOSplit) {
+    if (!canDoSplitModule(Mod) || !HasLargeCG(Mod, CombinedIndex)) {
+      ProfitableToSplit = false;
+      LLVM_DEBUG(dbgs() << "warning: thinlto split not enable for module: "
+                        << Mod.getName());
+    } else {
+      LLVM_DEBUG(dbgs() << "thinlto: split codegen for module: "
+                        << Mod.getName());
+    }
+  }
+
   LLVM_DEBUG(dbgs() << "Running ThinLTO\n");
   if (CodeGenOnly) {
-    // If CodeGenOnly is set, we only perform code generation and skip
-    // optimization. This value may differ from Conf.CodeGenOnly.
-    codegen(Conf, TM.get(), AddStream, Task, Mod, CombinedIndex);
+    if (ThinLTOSplit && ProfitableToSplit)
+      splitOptAndCodeGenThin(Task, Conf, TM.get(), AddStream,
+                             ThinLTOSplitPartitions, Mod, CombinedIndex,
+                             CmdArgs, false, IRAddStream, BitcodeLibFuncs);
+    else
+      // If CodeGenOnly is set, we only perform code generation and skip
+      // optimization. This value may differ from Conf.CodeGenOnly.
+      codegen(Conf, TM.get(), AddStream, Task, Mod, CombinedIndex);
     return finalizeOptimizationRemarks(std::move(DiagnosticOutputFile));
   }
 
@@ -690,20 +941,27 @@ Error lto::thinBackend(const Config &Conf, unsigned Task, AddStreamFn AddStream,
   auto OptimizeAndCodegen =
       [&](Module &Mod, TargetMachine *TM,
           LLVMRemarkFileHandle DiagnosticOutputFile) {
-        // Perform optimization and code generation for ThinLTO.
-        if (!opt(Conf, TM, Task, Mod, /*IsThinLTO=*/true,
-                 /*ExportSummary=*/nullptr, /*ImportSummary=*/&CombinedIndex,
-                 CmdArgs, BitcodeLibFuncs))
-          return finalizeOptimizationRemarks(std::move(DiagnosticOutputFile));
-
-        // Save the current module before the first codegen round.
-        // Note that the second codegen round runs only `codegen()` without
-        // running `opt()`. We're not reaching here as it's bailed out earlier
-        // with `CodeGenOnly` which has been set in `SecondRoundThinBackend`.
-        if (IRAddStream)
-          cgdata::saveModuleForTwoRounds(Mod, Task, IRAddStream);
-
-        codegen(Conf, TM, AddStream, Task, Mod, CombinedIndex);
+        if (ThinLTOSplit && ProfitableToSplit) {
+          if (!splitOptAndCodeGenThin(
+                  Task, Conf, TM, AddStream, ThinLTOSplitPartitions, Mod,
+                  CombinedIndex, CmdArgs, true, IRAddStream, BitcodeLibFuncs))
+            return finalizeOptimizationRemarks(std::move(DiagnosticOutputFile));
+        } else {
+          // Perform optimization and code generation for ThinLTO.
+          if (!opt(Conf, TM, Task, Mod, /*IsThinLTO=*/true,
+                  /*ExportSummary=*/nullptr, /*ImportSummary=*/&CombinedIndex,
+                  CmdArgs, BitcodeLibFuncs))
+            return finalizeOptimizationRemarks(std::move(DiagnosticOutputFile));
+
+          // Save the current module before the first codegen round.
+          // Note that the second codegen round runs only `codegen()` without
+          // running `opt()`. We're not reaching here as it's bailed out earlier
+          // with `CodeGenOnly` which has been set in `SecondRoundThinBackend`.
+          if (IRAddStream)
+            cgdata::saveModuleForTwoRounds(Mod, Task, IRAddStream);
+
+          codegen(Conf, TM, AddStream, Task, Mod, CombinedIndex);
+        }
         return finalizeOptimizationRemarks(std::move(DiagnosticOutputFile));
       };
 
diff --git a/llvm/lib/Transforms/Utils/CMakeLists.txt b/llvm/lib/Transforms/Utils/CMakeLists.txt
index 3b7a134b49e2e..ef7e05ce89e9d 100644
--- a/llvm/lib/Transforms/Utils/CMakeLists.txt
+++ b/llvm/lib/Transforms/Utils/CMakeLists.txt
@@ -91,6 +91,7 @@ add_llvm_component_library(LLVMTransformUtils
   SizeOpts.cpp
   SplitModule.cpp
   SplitModuleByCategory.cpp
+  SplitModuleCG.cpp
   StripNonLineTableDebugInfo.cpp
   SymbolRewriter.cpp
   UnifyLoopExits.cpp
diff --git a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
new file mode 100644
index 0000000000000..9f57cb3ed566e
--- /dev/null
+++ b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
@@ -0,0 +1,26 @@
+#include "llvm/Transforms/Utils/SplitModuleCG.h"
+
+using namespace llvm;
+
+#define DEBUG_TYPE "split-module-CG"
+
+void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
+                                const llvm::lto::Config &C) {
+  // TODO: 1. Process the linkage of the GlobalValue; 2. Allocate the callgraph
+  // to N partitions; 3.Invoke the cloneModule API to copy the N partitions to
+  // obtain MParts.
+
+}
+
+SplitModuleCG::SplitModuleCG(Module &M,
+                             const ModuleSummaryIndex &CombinedIndex,
+                             unsigned LimitPartition)
+    : M(M), CG(M), N(LimitPartition) {
+  // TODO: The module is split based on the callgraph, and EntryFuncs stores
+  // the root function of each callgraph.
+
+  if (N == 0 || N > EntryFuncs.size()) {
+    N = EntryFuncs.size();
+  }
+  N = N == 0 ? 1 : N;
+}

>From e9131d99641e122924617e83bc70623f0e35e98b Mon Sep 17 00:00:00 2001
From: maojiaping <maojiaping1 at huawei.com>
Date: Wed, 20 May 2026 15:27:29 +0800
Subject: [PATCH 02/13] [Thinlto][Split] Add callgraph-based module
 splitting(SplitModuleCG)

Add a new SplitModuleCG that partitions a module into multiple
parts using function callgraph traversal and cost-based load balancing.
This is intended for use in thinLTO to parallelize code generation by
splitting the module while preserving function call dependencies.

Key features:
- Build a simplified callgraph to track function calls and roots
- Calculate function costs based on IR instruction count
- Partition functions with balanced cost distribution
- Externalize local symbols and rename promoted symbols to avoid
  conflicts
- Clone module partitions and emit them in parallel
---
 .../llvm/Transforms/Utils/SplitModuleCG.h     | 182 ++++++++-
 llvm/lib/LTO/LTOBackend.cpp                   |  10 +
 llvm/lib/Transforms/Utils/SplitModuleCG.cpp   | 367 +++++++++++++++++-
 3 files changed, 552 insertions(+), 7 deletions(-)

diff --git a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
index e60c4e931d40c..956a1ea8030fe 100644
--- a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
+++ b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
@@ -1,6 +1,7 @@
 #ifndef LLVM_TRANSFORMS_UTILS_SPLITMODULECG_H
 #define LLVM_TRANSFORMS_UTILS_SPLITMODULECG_H
 
+#include "llvm/ADT/StringSet.h"
 #include "llvm/Analysis/CallGraph.h"
 #include "llvm/Analysis/ModuleSummaryAnalysis.h"
 #include "llvm/LTO/Config.h"
@@ -8,6 +9,169 @@
 #include "llvm/ADT/DenseSet.h"
 
 namespace llvm {
+
+class SimplifyCallGraph;
+class SimplifyCallGraphNode;
+
+using CostType = InstructionCost::CostType;
+
+class SimplifyCallGraph {
+  using FunctionMapTy =
+      std::map<const Function *, std::unique_ptr<SimplifyCallGraphNode>>;
+
+  /// A map from \c Function* to \c SimplifyCallGraphNode*.
+  FunctionMapTy FunctionMap;
+
+public:
+  explicit SimplifyCallGraph(CallGraph &CG,
+                             const ModuleSummaryIndex &CombinedIndex,
+                             Module &M)
+      : CG(CG), M(M) {
+    createSimplifyCallGraph(CombinedIndex);
+  }
+  ~SimplifyCallGraph() {};
+
+  using iterator = FunctionMapTy::iterator;
+  using const_iterator = FunctionMapTy::const_iterator;
+
+  /// Returns the module the call graph corresponds to.
+  inline iterator begin() { return FunctionMap.begin(); }
+  inline iterator end() { return FunctionMap.end(); }
+  inline const_iterator begin() const { return FunctionMap.begin(); }
+  inline const_iterator end() const { return FunctionMap.end(); }
+
+  /// Returns the call graph node for the provided function.
+  inline const SimplifyCallGraphNode *operator[](const Function *F) const {
+    const_iterator I = FunctionMap.find(F);
+    assert(I != FunctionMap.end() && "Function not in callgraph!");
+    return I->second.get();
+  }
+
+  /// Returns the call graph node for the provided function.
+  inline SimplifyCallGraphNode *operator[](const Function *F) {
+    const_iterator I = FunctionMap.find(F);
+    assert(I != FunctionMap.end() && "Function not in callgraph!");
+    return I->second.get(); 
+  }
+
+  /// Returns the call graph node for the provided function.
+  inline const SimplifyCallGraphNode *at(const Function *F) const {
+    const_iterator I = FunctionMap.find(F);
+    assert(I != FunctionMap.end() && "Function not in callgraph!");
+    return I->second.get();
+  }
+
+  /// Returns the call graph node for the provided function.
+  inline SimplifyCallGraphNode *at(const Function *F) {
+    const_iterator I = FunctionMap.find(F);
+    assert(I != FunctionMap.end() && "Function not in callgraph!");
+    return I->second.get();
+  }
+
+  void createSimplifyCallGraph(const ModuleSummaryIndex &CombinedIndex);
+  void print();
+  SimplifyCallGraphNode *getOrInsertFunction(const Function *F);
+
+private:
+  CallGraph &CG;
+  Module &M;
+};
+
+class SimplifyCallGraphNode {
+public:
+  using CalledFunctionsSet = DenseSet<SimplifyCallGraphNode *>;
+  inline SimplifyCallGraphNode(SimplifyCallGraph *SCG, Function *F)
+      : SCG(SCG), F(F) {}
+
+  SimplifyCallGraphNode(const SimplifyCallGraphNode &) = delete;
+  SimplifyCallGraphNode &operator=(const SimplifyCallGraphNode &) = delete;
+
+  ~SimplifyCallGraphNode() {}
+
+  Function *getFunction() const { return F; }
+
+  unsigned getNumReferences() const { return NumReferences; }
+
+  using iterator = DenseSet<SimplifyCallGraphNode *>::iterator;
+  using const_iterator = DenseSet<SimplifyCallGraphNode *>::const_iterator;
+
+  inline iterator begin() { return CalledFunctions.begin(); }
+  inline iterator end() { return CalledFunctions.end(); }
+  inline const_iterator begin() const { return CalledFunctions.begin(); }
+  inline const_iterator end() const { return CalledFunctions.end(); }
+  inline size_t count(SimplifyCallGraphNode * SCGNode) { return CalledFunctions.count(SCGNode); }
+  inline bool empty() const { return CalledFunctions.empty(); }
+  inline unsigned size() const { return (unsigned)CalledFunctions.size(); }
+
+  void addCalledFunction(SimplifyCallGraphNode *Called) {
+    auto [It, Inserted] = CalledFunctions.insert(Called);
+    if (Inserted)
+      Called->AddRef();
+  }
+
+  void removeCalledFunction(SimplifyCallGraphNode *Called) {
+    auto NumRemoved = CalledFunctions.erase(Called);
+    if (NumRemoved > 0)
+      Called->DropRef();
+  }
+
+private:
+  friend class SimplifyCallGraph;
+
+  SimplifyCallGraph *SCG;
+  Function *F;
+
+  DenseSet<SimplifyCallGraphNode *> CalledFunctions;
+  unsigned NumReferences = 0;
+
+  void DropRef() { --NumReferences; }
+  void AddRef() { ++NumReferences; }
+};
+
+static void addAllDependencies(SimplifyCallGraph &SCG, const Function &F,
+                               DenseSet<const Function *> &Fns) {
+  assert(!F.isDeclaration());
+  SmallVector<const Function *> WorkList({&F});
+
+  while (!WorkList.empty()) {
+    const auto &CurFn = *WorkList.pop_back_val();
+    assert(!CurFn.isDeclaration());
+
+    // Scan for an indirect call. If such a call is found, we have to
+    // conservatively assume this can call all non-entrypoint functions in 
+    // the module.
+    for (auto &SCGNode : *SCG.at(&CurFn)) {
+      auto *Callee = SCGNode->getFunction();
+      if (!Callee || Callee->isDeclaration())
+        continue;
+      if (Callee != &F)
+      {
+        auto [It, Inserted] = Fns.insert(Callee);
+        if (Inserted)
+          WorkList.push_back(Callee);
+      }
+    }
+  }
+}
+
+struct FunctionWithDependencies {
+  FunctionWithDependencies(SimplifyCallGraph &SCG,
+                           const DenseMap<const Function *, CostType> &FnCosts,
+                           const Function *F)
+      : F(F) {
+    addAllDependencies(SCG, *F, Dependencies);
+
+    TotalCost = FnCosts.at(F);
+    for (const auto *Dep : Dependencies) {
+      TotalCost += FnCosts.lookup(Dep);
+    }
+  }
+
+  const Function *F = nullptr;
+  DenseSet<const Function *> Dependencies;
+  CostType TotalCost = 0;
+};
+
 /// Splits the module M into N linkable partitions. The function ModuleCallback
 /// is called N times passing each individual partition as the MPart argument.
 class SplitModuleCG {
@@ -21,12 +185,28 @@ class SplitModuleCG {
                    const llvm::lto::Config &C);
 
   unsigned getPartitionNum() { return N; }
+  StringSet<> &getOriginalExternals() { return OriginalExternals; }
+  StringMap<std::string> &getPromotedRenames() { return PromotedRenames; }
 
-  private:
+private:
   unsigned N;
   Module &M;
   CallGraph CG;
+  std::unique_ptr<SimplifyCallGraph> SCG;
+  CostType ModuleCost;
   DenseSet<const Function *> EntryFuncs;
+  StringSet<> OriginalExternals;
+  StringMap<std::string> PromotedRenames;
+  DenseMap<const Function *, bool> externalFunction;
+  DenseMap<const Function *, CostType> FuncsCosts;
+  SmallVector<FunctionWithDependencies> FWDWorkList;
+
+  void calculateFunctionCosts();
+  std::vector<DenseSet<const Function *>> doPartitioning();
+  void dealWithMpart(
+      Module &MPart, unsigned I,
+      function_ref<bool(const GlobalValue *)> NeedsConservativeImport);
+  void createWorkList();
 };
 
 } // end namespace llvm
diff --git a/llvm/lib/LTO/LTOBackend.cpp b/llvm/lib/LTO/LTOBackend.cpp
index 10d66a1a6097b..3e7d2c848ac7c 100644
--- a/llvm/lib/LTO/LTOBackend.cpp
+++ b/llvm/lib/LTO/LTOBackend.cpp
@@ -657,6 +657,16 @@ static bool splitOptAndCodeGenThin(unsigned task, const Config &C,
         cgdata::saveModuleForTwoRounds(*MPart, task + CurrentThreadId,
                                        IRAddStream);
     }
+    
+    // Rename the GlobalValues whose internal is changed to external. That's
+    // can avoid duplicate symbols.
+    auto PromotedRenames = SplitModuleCG.getPromotedRenames();
+    for (auto &GV : MPart->global_values()) {
+      if (auto It = PromotedRenames.find(GV.getName());
+          It != PromotedRenames.end()) {
+        GV.setName(It->second);
+      }
+    }
 
     auto splitStream = [&](unsigned task, const Twine &moduleName)
         -> Expected<std::unique_ptr<CachedFileStream>> {
diff --git a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
index 9f57cb3ed566e..debdddfb79041 100644
--- a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
+++ b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
@@ -1,26 +1,381 @@
 #include "llvm/Transforms/Utils/SplitModuleCG.h"
-
+#include "llvm/Bitcode/BitcodeReader.h"
+#include "llvm/Bitcode/BitcodeWriter.h"
+#include "llvm/IR/Function.h"
+#include "llvm/IR/GlobalValue.h"
+#include "llvm/IR/Module.h"
+#include "llvm/IR/Value.h"
+#include "llvm/Support/MD5.h"
+#include "llvm/Transforms/Utils/Cloning.h"
+#include <thread>
 using namespace llvm;
 
 #define DEBUG_TYPE "split-module-CG"
 
+namespace {
+
+static cl::opt<bool> enablePrintSimplifyCallGraph(
+    "enable-print-simplify-callgraph", cl::Hidden, cl::init(false),
+    cl::desc("print SimplifyCallGraph"));
+
+using PartitionID = unsigned;
+
+static void externalize(GlobalValue *GV) {
+  if (GV->hasLocalLinkage()) {
+    GV->setLinkage(GlobalValue::ExternalLinkage);
+    GV->setVisibility(GlobalValue::HiddenVisibility);
+  }
+
+  // Unnamed entities must be named consistently between modules. setName will
+  // give a distinct name to each such entity.
+  if (!GV->hasName())
+    GV->setName("__llvmsplit_unnamed");
+}
+
+} // namespace
+
+std::vector<DenseSet<const Function *>> SplitModuleCG::doPartitioning() {
+  LLVM_DEBUG(dbgs() << "\n--Partitioning Starts--\n");
+  // Performs all of the partitioning work on M.
+  std::vector<DenseSet<const Function *>> Partitions;
+  Partitions.resize(N);
+  if (N == 0)
+    return Partitions;
+
+  auto ComparePartitions = [](const std::pair<PartitionID, CostType> &a,
+                              const std::pair<PartitionID, CostType> &b) {
+    // When two partitions have the same cost, assign to the one with the
+    // biggest ID first. This allows us to put things in P0 last, because P0 may
+    // have other stuff added later.
+    if (a.second == b.second)
+      return a.first < b.first;
+    return a.second > b.second;
+  };
+
+  std::vector<std::pair<PartitionID, CostType>> BalancingQueue;
+  for (unsigned I = 0; I < N; ++I)
+    BalancingQueue.emplace_back(I, 0);
+
+  // Helper function to handle assigning a function to a partition. This takes
+  // care of updating the balancing queue.
+  const auto AssignToPartition = [&](PartitionID PID,
+                                     const FunctionWithDependencies &FWD) {
+    auto &FnsInPart = Partitions[PID];
+    FnsInPart.insert(FWD.F);
+    for (const Function *Dep : FWD.Dependencies) {
+      FnsInPart.insert(Dep);
+    }
+
+    // Update the balancing queue. we scan backwards because in the common case
+    // the partition is at the end.
+    for (auto &[QueuePID, Cost] : reverse(BalancingQueue)) {
+      if (QueuePID == PID) {
+        CostType NewCost = 0;
+        for (auto *Fn : Partitions[PID])
+          NewCost += FuncsCosts.at(Fn);
+        Cost = NewCost;
+      }
+    }
+
+    sort(BalancingQueue, ComparePartitions);
+  };
+
+  for (auto &CurFn : FWDWorkList) {
+    // Normal "load-balancing", assign to partition with least pressure.
+    auto [PID, CurCost] = BalancingQueue.back();
+    AssignToPartition(PID, CurFn);
+  }
+
+  return Partitions;
+}
+
+void SplitModuleCG::calculateFunctionCosts() {
+  ModuleCost = 0;
+  for (auto &Fn : M) {
+    if (Fn.isDeclaration())
+      continue;
+
+    CostType FnCost = 0;
+    for (const auto &BB : Fn) {
+      CostType CostVal = std::distance(BB.begin(), BB.end());
+      FnCost += CostVal;
+    }
+    assert(FnCost != 0);
+    FuncsCosts[&Fn] = FnCost;
+    assert((ModuleCost + FnCost) >= ModuleCost && "Overflow!");
+    ModuleCost += FnCost;
+  }
+}
+
+void SplitModuleCG::dealWithMpart(Module &MPart, unsigned I,
+                                  function_ref<bool(const GlobalValue *)> NeedsConservativeImport) {
+  // collect symbols to rename
+  auto checkPromoted = [&](const GlobalValue &GV) {
+    // now is external (not local), but not in external set.
+    if (!GV.hasLocalLinkage() && !OriginalExternals.contains(GV.getName())) {
+      if (PromotedRenames.count(GV.getName()))
+        return;
+      MD5 Hash;
+      Hash.update(M.getModuleIdentifier());
+      MD5::MD5Result Result;
+      Hash.final(Result);
+      SmallString<32> HashStr;
+      MD5::stringifyResult(Result, HashStr);
+      std::string NewName = (GV.getName() + "." + HashStr.str().substr(0, 8)).str();
+      PromotedRenames[GV.getName()] = NewName;
+    }
+  };
+
+  auto AvailableExternalizeFunc = [&](llvm::Function &Func) {
+    Func.setLinkage(GlobalValue::AvailableExternallyLinkage);
+    Func.setComdat(nullptr);
+  };
+
+  for (const auto &GV : MPart.global_values())
+    checkPromoted(GV);
+  // Clean-up conservatively imported GVs without any users.
+  for (auto &GV : make_early_inc_range(MPart.globals())) {
+    if (NeedsConservativeImport(&GV) && GV.use_empty())
+      GV.eraseFromParent();
+  }
+
+  for (auto &func : MPart.functions()) {
+    auto Fn = M.getFunction(func.getName());
+    if (externalFunction.count(Fn) && !func.isDeclaration()) {
+      if (!externalFunction[Fn]) {
+        AvailableExternalizeFunc(func);
+      } else {
+        externalFunction[Fn] = false;
+      }
+    }
+  }
+
+  LLVM_DEBUG(dbgs() << MPart.getModuleIdentifier() << "  : \n");
+  for (auto &F : MPart) {
+    if (!F.isDeclaration())
+      LLVM_DEBUG(dbgs() << "   [Function: ] " << I << "  " << F.getName() << " "
+                        << F.getLinkage() << "\n");
+  }
+}
+
+void SplitModuleCG::createWorkList() {
+  // First, find all the entry functions with an in-degree of 0
+  // (i.e., those that are not called by any function).
+  for (auto &NodePair : *SCG) {
+    SimplifyCallGraphNode *SCGNode = NodePair.second.get();
+    Function *F = SCGNode->getFunction();
+    if (F && SCGNode->getNumReferences() == 0) {
+      EntryFuncs.insert(F);
+    }
+  }
+
+  // Second, find all the dependencies of each entry function.
+  for (auto *F : EntryFuncs) {
+    FWDWorkList.emplace_back(*SCG, FuncsCosts, F);
+  }
+
+  // Third, find all the functions that are not in the worklist.
+  DenseSet<const Function *> SeenFunctions;
+  for (const auto &FWD : FWDWorkList) {
+    SeenFunctions.insert(FWD.F);
+    SeenFunctions.insert(FWD.Dependencies.begin(), FWD.Dependencies.end());
+  }
+  for (auto &F : M) {
+    // This function may be in a ring, and therefore is not a dependency of
+    // any root, which is treated as a root function here.
+    if (!F.isDeclaration() && !SeenFunctions.count(&F)) {
+      FWDWorkList.emplace_back(*SCG, FuncsCosts, &F);
+      auto &FWD = FWDWorkList.back();
+      EntryFuncs.insert(&F);
+      SeenFunctions.insert(FWD.F);
+      SeenFunctions.insert(FWD.Dependencies.begin(), FWD.Dependencies.end());
+    }
+  }
+
+  // Sort the worklist so the most expensive roots are seen first.
+  sort(FWDWorkList, [&](auto &A, auto &B) {
+    // Sort by total cost, and if the total cost is identical, sort
+    // alphabetically
+    if (A.TotalCost == B.TotalCost)
+      return A.F->getName() < B.F->getName();
+    return A.TotalCost > B.TotalCost;
+  });
+
+  LLVM_DEBUG(dbgs() << "Number of callgraphs to be allocated: "
+                    << FWDWorkList.size() << "   Module cost: "
+                    << ModuleCost << "\n");
+  LLVM_DEBUG(dbgs() << "callgraphs: \n");
+  for (auto FWD : FWDWorkList) {
+    LLVM_DEBUG(dbgs() << "[root] " << FWD.F->getName() << " (totalCost:"
+                      << FWD.TotalCost << ";   root function cost: "
+                      << FuncsCosts[FWD.F] << ";   has dependency: "
+                      << FWD.Dependencies.size() << "\n");
+  }
+}
+
 void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
                                 const llvm::lto::Config &C) {
-  // TODO: 1. Process the linkage of the GlobalValue; 2. Allocate the callgraph
-  // to N partitions; 3.Invoke the cloneModule API to copy the N partitions to
-  // obtain MParts.
+  for (Function &F : M) {
+    if (F.hasLocalLinkage() && F.hasOneUse() && !F.hasAddressTaken())
+      continue;
+    externalize(&F);
+    if (!F.isDeclaration() &&
+        (F.hasExternalLinkage() || !F.isDefinitionExact()))
+      externalFunction[&F] = true;
+  }
+  for (GlobalVariable &GV : M.globals())
+    externalize(&GV);
+  for (GlobalAlias &GA : M.aliases())
+    externalize(&GA);
+  for (GlobalIFunc &GI : M.ifuncs())
+    externalize(&GI);
 
+  // TODO: Consider optimizing the alias, replacing the determined alias with
+  // the determined aliasee.
+
+  // Assign callgraphs into N partitions.
+  auto Partitions = doPartitioning();
+  assert(Partitions.size() == N);
+
+  // local GVs need to be conservatively imported into [dependency] every module,
+ 	// and then cleaned up afterwards.
+  const auto NeedsConservativeImport = [&](const GlobalValue *GV) {
+    // We conservatively import private/internal GVs into every module and clean
+    // them up afterwards.
+    const auto *Var = dyn_cast<GlobalVariable>(GV);
+    return Var && Var->hasLocalLinkage();
+  };
+
+  auto ShouldCloneDefinition = [&](unsigned I, const GlobalValue *GV) {
+    const auto &FnsInPart = Partitions[I];
+
+    // Functions go in their assigned partition.
+    if (const auto *newFn = dyn_cast<Function>(GV)) {
+      const auto *Fn = M.getFunction(newFn->getName());
+      return FnsInPart.contains(Fn);
+    }
+    if (NeedsConservativeImport(GV))
+      return true;
+    // Everything else goes in the first partition.
+    return I == 0;
+  };
+
+  // TODO: In the future, it may be considered to also include clonemodule in
+  // parallel to reduce compilation time.
+  std::vector<std::thread> Threads;
+  Threads.reserve(N);
+  std::vector<std::unique_ptr<Module>> MPartInCtxs;
+  MPartInCtxs.resize(N);
+  for (unsigned I = 0; I < N; ++I) {
+    ValueToValueMapTy VMap;
+    std::unique_ptr<Module> MPart(
+      CloneModule(M, VMap, [&](const GlobalValue *GV) {
+        return ShouldCloneDefinition(I, GV);
+    }));
+
+    dealWithMpart(*MPart, I, NeedsConservativeImport);
+
+    // If not clone module in multi-thread, we also need to clone
+    // the module obtained through segmentation into a new context
+    // to avoid data races.
+    SmallString<0> BC;
+    raw_svector_ostream BCOS(BC);
+    WriteBitcodeToFile(*MPart, BCOS);
+    MPart.reset();
+    Threads.emplace_back([&, I](SmallString<0> BC) {
+      llvm::lto::LTOLLVMContext Ctx(C);
+      Expected<std::unique_ptr<Module>> MOrErr = parseBitcodeFile(
+          MemoryBufferRef(BC.str(), "ld-temp.o"), Ctx);
+      BC = SmallString<0>();
+      if (!MOrErr)
+        report_fatal_error("Failed to read bitcode");
+      ModuleCallback(std::move(MOrErr.get()), I);
+    }, std::move(BC));
+  }
+  for (auto &T : Threads)
+    T.join();
 }
 
 SplitModuleCG::SplitModuleCG(Module &M,
                              const ModuleSummaryIndex &CombinedIndex,
                              unsigned LimitPartition)
     : M(M), CG(M), N(LimitPartition) {
-  // TODO: The module is split based on the callgraph, and EntryFuncs stores
-  // the root function of each callgraph.
+  // Track existing non-local symbols. This ensures that when we promote
+  // internal symbols to external for partitioning, we can handle renaming
+  // and avoid conflicts.
+  for (const auto &GV : M.global_values())
+    if (!GV.hasLocalLinkage())
+      OriginalExternals.insert(GV.getName());
+
+  calculateFunctionCosts();
+
+  // Construct a simplified call graph to facilitate worklist generation.
+  SCG = std::make_unique<SimplifyCallGraph>(CG, CombinedIndex, M);
+  // TODO: When the SCG is established, the special cases of comdat and
+  // initarray need to be considered.
+
+  // Populate the worklist with root functions and their transitive
+  // dependencies. This worklist serves as the foundation for the
+  // subsequent module partitioning.
+  createWorkList();
 
   if (N == 0 || N > EntryFuncs.size()) {
     N = EntryFuncs.size();
   }
   N = N == 0 ? 1 : N;
 }
+
+void SimplifyCallGraph::createSimplifyCallGraph(
+    const ModuleSummaryIndex &CombinedIndex) {
+  for (auto &NodePair : CG) {
+    CallGraphNode *CGNode = NodePair.second.get();
+    Function *F = CGNode->getFunction();
+    if (!F || F->isDeclaration())
+      continue;
+
+    SimplifyCallGraphNode *SCGNode = getOrInsertFunction(F);
+
+    //TODO: Trace indirect call usage for the current function.
+
+    for (const auto &CGNodeItem : *CGNode) {
+      Function *Called = CGNodeItem.second->getFunction();
+      if (!Called) {
+        //TODO: Deal with indirect call. 
+        // 1. Check if the instruction has a callees metadata.
+        // 2. Check if this is an indirect call with profile data.
+        // 3. Check if this is an alias to a function.
+      }
+      if (!Called || Called->isDeclaration())
+        continue;
+      SCGNode->addCalledFunction(getOrInsertFunction(Called));
+    }
+  }
+
+  if (enablePrintSimplifyCallGraph)
+    print();
+}
+
+
+void SimplifyCallGraph::print() {
+  for (auto &SCGItem : FunctionMap) {
+    LLVM_DEBUG(dbgs() << "Call graph node for function: '"
+                      << SCGItem.first->getName() << "' #uses="
+                      << SCGItem.second->getNumReferences() << "\n");
+
+    for (const auto &callee : *SCGItem.second) {
+      LLVM_DEBUG(dbgs() <<"          Calls function : '"
+                        << callee->getFunction()->getName() << " '\n");
+    }
+  }
+}
+
+SimplifyCallGraphNode *
+SimplifyCallGraph::getOrInsertFunction(const Function *F) {
+  auto &SCGN = FunctionMap[F];
+  if (SCGN)
+    return SCGN.get();
+
+  SCGN =
+      std::make_unique<SimplifyCallGraphNode>(this, const_cast<Function *>(F));
+  return SCGN.get();
+}

>From e579947ba21044268ed1fb57a394aa3e9836d603 Mon Sep 17 00:00:00 2001
From: maojiaping <maojiaping1 at huawei.com>
Date: Wed, 20 May 2026 15:57:13 +0800
Subject: [PATCH 03/13] [llvm-split][SplitModuleCG] Add support for
 SplitModuleCG

Add a new command line option --enable-split-module-CG to llvm-split
tool for testing the SplitModuleCG utility.

The change:
- Adds --enable-split-module-CG flag
- Wire up the SplitModuleCG interface in llvm-split
---
 .../SplitModuleCG/split-promoted-rename.ll    | 41 +++++++++++++++++++
 .../SplitModuleCG/function-with-ring.ll       | 36 ++++++++++++++++
 .../llvm-split/SplitModuleCG/function.ll      | 35 ++++++++++++++++
 .../llvm-split/SplitModuleCG/partition-cap.ll | 10 +++++
 .../SplitModuleCG/single-partition.ll         | 13 ++++++
 .../tools/llvm-split/SplitModuleCG/unnamed.ll |  8 ++++
 llvm/tools/llvm-split/llvm-split.cpp          | 36 ++++++++++++++++
 7 files changed, 179 insertions(+)
 create mode 100644 llvm/test/Transforms/SplitModuleCG/split-promoted-rename.ll
 create mode 100644 llvm/test/tools/llvm-split/SplitModuleCG/function-with-ring.ll
 create mode 100644 llvm/test/tools/llvm-split/SplitModuleCG/function.ll
 create mode 100644 llvm/test/tools/llvm-split/SplitModuleCG/partition-cap.ll
 create mode 100644 llvm/test/tools/llvm-split/SplitModuleCG/single-partition.ll
 create mode 100644 llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll

diff --git a/llvm/test/Transforms/SplitModuleCG/split-promoted-rename.ll b/llvm/test/Transforms/SplitModuleCG/split-promoted-rename.ll
new file mode 100644
index 0000000000000..6c51141a9ad85
--- /dev/null
+++ b/llvm/test/Transforms/SplitModuleCG/split-promoted-rename.ll
@@ -0,0 +1,41 @@
+; Test that internal symbols promoted during module splitting are consistently
+; renamed with an MD5 suffix across all partitions.
+;
+; RUN: opt -module-summary %s -o %t.bc
+; RUN: llvm-lto2 run %t.bc -o %t \
+; RUN:   -thinlto-split=true \
+; RUN:   -thinlto-split-partitions=2 -thinlto-split-module-size-threshold=0 \
+; RUN:   -r=%t.bc,caller_a,px \
+; RUN:   -r=%t.bc,caller_b,px
+; RUN: llvm-nm %t.1 | FileCheck %s
+
+; CHECK-DAG: T caller_a
+; CHECK-DAG: T caller_b
+; CHECK:     T {{.*promoted_internal[._][0-9a-f]+.*}}
+; CHECK-NOT: T promoted_internal{{$}}
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; @promoted_internal is internal. SplitModuleCG::dealWithMpart's checkPromoted
+; records it in PromotedRenames. splitOptAndCodeGenThin applies the rename
+; after opt via:
+;   for (auto &GV : MPart->global_values())
+;     if (auto It = PromotedRenames.find(GV.getName()); ...)
+;       GV.setName(It->second);
+define internal void @promoted_internal() {
+entry:
+  ret void
+}
+
+define void @caller_a() {
+entry:
+  call void @promoted_internal()
+  ret void
+}
+
+define void @caller_b() {
+entry:
+  call void @promoted_internal()
+  ret void
+}
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/function-with-ring.ll b/llvm/test/tools/llvm-split/SplitModuleCG/function-with-ring.ll
new file mode 100644
index 0000000000000..f2fc8c03c922a
--- /dev/null
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/function-with-ring.ll
@@ -0,0 +1,36 @@
+; RUN: llvm-split -enable-split-module-CG=true -j2 -o %t %s
+; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
+; RUN: llvm-dis -o - %t1 | FileCheck --check-prefix=CHECK1 %s
+
+; CHECK0-DAG: declare void @foo()
+; CHECK0-DAG: define void @bar()
+; CHECK0-DAG: declare void @call_foo()
+; CHECK0-DAG: define void @call_bar()
+
+; CHECK1-DAG: define void @foo()
+; CHECK1-DAG: declare void @bar()
+; CHECK1-DAG: define void @call_foo()
+; CHECK1-DAG: declare void @call_bar()
+
+define void @foo() {
+entry:
+  call void @call_foo()
+  ret void
+}
+
+define void @bar() {
+entry:
+  ret void
+}
+
+define void @call_foo() {
+entry:
+  call void @foo()
+  ret void
+}
+
+define void @call_bar() {
+entry:
+  call void @bar()
+  ret void
+}
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/function.ll b/llvm/test/tools/llvm-split/SplitModuleCG/function.ll
new file mode 100644
index 0000000000000..ddf5bb5c3dff3
--- /dev/null
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/function.ll
@@ -0,0 +1,35 @@
+; RUN: llvm-split -enable-split-module-CG=true -j2 -o %t %s
+; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
+; RUN: llvm-dis -o - %t1 | FileCheck --check-prefix=CHECK1 %s
+
+; CHECK0-DAG: declare dso_local void @foo()
+; CHECK0-DAG: define void @bar()
+; CHECK0-DAG: declare void @func_a()
+; CHECK0-DAG: define void @func_b()
+; CHECK1-DAG: define internal void @foo()
+; CHECK1-DAG: define available_externally void @bar()
+; CHECK1-DAG: define void @func_a()
+; CHECK1-DAG: declare void @func_b()
+
+define internal void @foo() {
+entry:
+  ret void
+}
+
+define void @bar() {
+entry:
+  ret void
+}
+
+define void @func_a() {
+entry:
+  call void @foo()
+  call void @bar()
+  ret void
+}
+
+define void @func_b() {
+entry:
+  call void @bar()
+  ret void
+}
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/partition-cap.ll b/llvm/test/tools/llvm-split/SplitModuleCG/partition-cap.ll
new file mode 100644
index 0000000000000..5c3ced3e682af
--- /dev/null
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/partition-cap.ll
@@ -0,0 +1,10 @@
+; RUN: llvm-split -enable-split-module-CG=true -j10 -o %t %s
+; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
+; RUN: llvm-dis -o - %t1 | FileCheck --check-prefix=CHECK1 %s
+; should only produce 2 output files (N capped to EntryFuncs.size()=2)
+
+; CHECK0: define void @foo()
+; CHECK1: define void @bar()
+
+define void @foo() { ret void }
+define void @bar() { ret void }
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/single-partition.ll b/llvm/test/tools/llvm-split/SplitModuleCG/single-partition.ll
new file mode 100644
index 0000000000000..fdfdf910a3498
--- /dev/null
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/single-partition.ll
@@ -0,0 +1,13 @@
+; RUN: llvm-split -enable-split-module-CG=true -j1 -o %t %s
+; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
+
+; CHECK0: define void @foo()
+; CHECK0: define void @bar()
+
+define void @foo() {
+  call void @bar()
+  ret void
+}
+define void @bar() {
+  ret void
+}
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll b/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll
new file mode 100644
index 0000000000000..73f7079669c55
--- /dev/null
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll
@@ -0,0 +1,8 @@
+; RUN: llvm-split -enable-split-module-CG=true -j2 -o %t %s
+; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
+
+; CHECK0-DAG: define hidden void @__llvmsplit_unnamed()
+
+define internal void @0() {
+  ret void
+}
\ No newline at end of file
diff --git a/llvm/tools/llvm-split/llvm-split.cpp b/llvm/tools/llvm-split/llvm-split.cpp
index 4ead6fd4b88be..9294ce1e4c9f1 100644
--- a/llvm/tools/llvm-split/llvm-split.cpp
+++ b/llvm/tools/llvm-split/llvm-split.cpp
@@ -18,8 +18,10 @@
 #include "llvm/IR/LLVMContext.h"
 #include "llvm/IR/PassInstrumentation.h"
 #include "llvm/IR/PassManager.h"
+#include "llvm/IR/ModuleSummaryIndex.h"
 #include "llvm/IR/Verifier.h"
 #include "llvm/IRReader/IRReader.h"
+#include "llvm/LTO/Config.h"
 #include "llvm/MC/TargetRegistry.h"
 #include "llvm/Support/CommandLine.h"
 #include "llvm/Support/FileSystem.h"
@@ -35,6 +37,7 @@
 #include "llvm/Transforms/IPO/GlobalDCE.h"
 #include "llvm/Transforms/Utils/SplitModule.h"
 #include "llvm/Transforms/Utils/SplitModuleByCategory.h"
+#include "llvm/Transforms/Utils/SplitModuleCG.h"
 
 using namespace llvm;
 
@@ -76,6 +79,10 @@ static cl::opt<std::string>
 static cl::opt<std::string>
     MCPU("mcpu", cl::desc("Target CPU, ignored if --mtriple is not used"),
          cl::value_desc("cpu"), cl::cat(SplitCategory));
+         
+static cl::opt<bool>
+    EnableSplitModuleCG("enable-split-module-CG", cl::Prefix, cl::init(false),
+     cl::desc("Split module using call graph"), cl::cat(SplitCategory));
 
 enum class SplitByCategoryType {
   SBCT_ByAttribute,
@@ -324,6 +331,35 @@ int main(int argc, char **argv) {
               "splitModule implementation\n";
   }
 
+  if (EnableSplitModuleCG) {
+    const auto HandleModulePartCG = [&](std::unique_ptr<Module> MPart, unsigned I) {
+      std::error_code EC;
+      std::unique_ptr<ToolOutputFile> Out(
+          new ToolOutputFile(OutputFilename + utostr(I), EC, sys::fs::OF_None));
+      if (EC) {
+        errs() << EC.message() << '\n';
+        exit(1);
+      }
+
+      if (verifyModule(*MPart, &errs())) {
+        errs() << "Broken module!\n";
+        exit(1);
+      }
+
+      WriteBitcodeToFile(*MPart, Out->os());
+
+      // Declare success.
+      Out->keep();
+    };
+
+    llvm::lto::Config Config;
+    ModuleSummaryIndex CombinedIndex(false);
+    SplitModuleCG SplitModuleCG(*M, CombinedIndex, NumOutputs);
+    SplitModuleCG.SplitModule(HandleModulePartCG, Config);
+    return 0;
+  }
+
   SplitModule(*M, NumOutputs, HandleModulePart, PreserveLocals, RoundRobin);
   return 0;
 }
+

>From 89cf40e9f4fa09c0f21ee5067e56fcb69af3a030 Mon Sep 17 00:00:00 2001
From: maojiaping <maojiaping1 at huawei.com>
Date: Fri, 12 Jun 2026 15:17:07 +0800
Subject: [PATCH 04/13] [SplitModuleCG] Fix warning errors

- Remove unused variable.
- Fix constructor initialization order to match class
  declaration order (N, M, CG).
---
 llvm/include/llvm/Transforms/Utils/SplitModuleCG.h | 6 ++----
 llvm/lib/LTO/LTOBackend.cpp                        | 1 -
 llvm/lib/Transforms/Utils/SplitModuleCG.cpp        | 2 +-
 3 files changed, 3 insertions(+), 6 deletions(-)

diff --git a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
index 956a1ea8030fe..9836376b94a82 100644
--- a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
+++ b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
@@ -26,7 +26,7 @@ class SimplifyCallGraph {
   explicit SimplifyCallGraph(CallGraph &CG,
                              const ModuleSummaryIndex &CombinedIndex,
                              Module &M)
-      : CG(CG), M(M) {
+      : CG(CG) {
     createSimplifyCallGraph(CombinedIndex);
   }
   ~SimplifyCallGraph() {};
@@ -74,14 +74,13 @@ class SimplifyCallGraph {
 
 private:
   CallGraph &CG;
-  Module &M;
 };
 
 class SimplifyCallGraphNode {
 public:
   using CalledFunctionsSet = DenseSet<SimplifyCallGraphNode *>;
   inline SimplifyCallGraphNode(SimplifyCallGraph *SCG, Function *F)
-      : SCG(SCG), F(F) {}
+      : F(F) {}
 
   SimplifyCallGraphNode(const SimplifyCallGraphNode &) = delete;
   SimplifyCallGraphNode &operator=(const SimplifyCallGraphNode &) = delete;
@@ -118,7 +117,6 @@ class SimplifyCallGraphNode {
 private:
   friend class SimplifyCallGraph;
 
-  SimplifyCallGraph *SCG;
   Function *F;
 
   DenseSet<SimplifyCallGraphNode *> CalledFunctions;
diff --git a/llvm/lib/LTO/LTOBackend.cpp b/llvm/lib/LTO/LTOBackend.cpp
index 3e7d2c848ac7c..7774daa112e25 100644
--- a/llvm/lib/LTO/LTOBackend.cpp
+++ b/llvm/lib/LTO/LTOBackend.cpp
@@ -608,7 +608,6 @@ static bool splitOptAndCodeGenThin(unsigned task, const Config &C,
                                    const std::vector<uint8_t> &CmdArgs,
                                    bool DoOpt, AddStreamFn IRAddStream,
                                    ArrayRef<StringRef> &BitcodeLibFuncs) {
-  unsigned ThreadCount = 0;
   const Target *T = &TM->getTarget();
 
   static std::mutex PrintMutex;
diff --git a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
index debdddfb79041..c50111204e1f0 100644
--- a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
+++ b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
@@ -299,7 +299,7 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
 SplitModuleCG::SplitModuleCG(Module &M,
                              const ModuleSummaryIndex &CombinedIndex,
                              unsigned LimitPartition)
-    : M(M), CG(M), N(LimitPartition) {
+    : N(LimitPartition), M(M), CG(M) {
   // Track existing non-local symbols. This ensures that when we promote
   // internal symbols to external for partitioning, we can handle renaming
   // and avoid conflicts.

>From 9f756bd510aff9090169795e9d1ce84520360d5e Mon Sep 17 00:00:00 2001
From: maojiaping <maojiaping1 at huawei.com>
Date: Mon, 22 Jun 2026 09:09:02 +0800
Subject: [PATCH 05/13] [ThinLTO][SplitModuleCG] Trim non-core code

---
 llvm/lib/LTO/LTOBackend.cpp                   | 199 +-----------------
 .../SplitModuleCG/split-promoted-rename.ll    |  41 ----
 2 files changed, 10 insertions(+), 230 deletions(-)
 delete mode 100644 llvm/test/Transforms/SplitModuleCG/split-promoted-rename.ll

diff --git a/llvm/lib/LTO/LTOBackend.cpp b/llvm/lib/LTO/LTOBackend.cpp
index 7774daa112e25..725328d15b537 100644
--- a/llvm/lib/LTO/LTOBackend.cpp
+++ b/llvm/lib/LTO/LTOBackend.cpp
@@ -84,16 +84,6 @@ static cl::list<std::string>
                              "path matches this for -save-temps options"),
                     cl::CommaSeparated, cl::Hidden);
 
-static cl::opt<unsigned> ThinLTOSplitModuleSizeThreshold(
-    "thinlto-split-module-size-threshold", cl::Hidden, cl::init(500),
-    cl::desc("Control the amount of whether split in thinlto backend"
-             "accroding to the size of a module."));
-
-static cl::opt<float> ThinLTOSplitModuleSizeRateThreshold(
-    "thinlto-split-module-size-rate-threshold", cl::Hidden, cl::init(0.5),
-    cl::desc("Whether to split in thinlto backend based on the ratio of "
-             "(callgraph size)/(module size)"));
-
 static cl::opt<unsigned> ThinLTOSplitPartitions(
     "thinlto-split-partitions", cl::Hidden, cl::init(0),
     cl::desc("Control split to how many partitions in thinlto backend."));
@@ -540,66 +530,6 @@ static void codegen(const Config &Conf, TargetMachine *TM,
     report_fatal_error(std::move(Err));
 }
 
-static unsigned calFunctionSize(const llvm::Function &F) {
-  unsigned size = 0;
-  for (const auto &BB : F)
-    size += std::distance(BB.begin(), BB.end());
-  return size;
-}
-
-static unsigned calModuleSize(const llvm::Module &M) {
-  unsigned size = 0;
-  for (const auto &F : M)
-    size += calFunctionSize(F);
-  return size;
-}
-
-static bool canDoSplitModule(const llvm::Module &M) {
-  if (calModuleSize(M) < ThinLTOSplitModuleSizeThreshold)
-    return false;
-  return true;
-}
-
-static bool HasLargeCG(Module &Mod, const ModuleSummaryIndex &CombinedIndex) {
-  // TODO: Check whether there has large callgraphs. When multiple callgraphs
-  // are split, thinlto parallel compilation can bring benefits.
-  return true;
-}
-
-struct TaskIdAllocator {
-  using TaskId = unsigned;
-
-  // Use the most significant bit (MSB) as a namespace tag.
-  // - Original ThinLTO backend tasks are expected to have MSB == 0.
-  // - Split partitions allocated by this allocator always have MSB == 1.
-  // This guarantees the two ID spaces never overlap.
-  static constexpr TaskId tag() {
-    return TaskId{1} << (std::numeric_limits<TaskId>::digits - 1);
-  }
-
-  // Monotonic sequence counter for split partitions (MSB must remain 0 here).
-  std::atomic<TaskId> seq{0};
-
-  // Allocate a globally unique TaskId for a split partition.
-  // The returned ID is `tag() | seq`, so it lives in the MSB==1 namespace.
-  TaskId alloc() {
-    TaskId v = seq.fetch_add(1, std::memory_order_relaxed);
-
-    // If the counter ever reaches the MSB, we'd overlap namespaces.
-    // This indicates an overflow / too many partitions.
-    if (v & tag())
-      report_fatal_error("Partition TaskId overflow: seq reached the tag bit.");
-
-    return tag() | v;
-  }
-
-  // Helper for sanity checks / debugging.
-  static bool isPartition(TaskId id) { return (id & tag()) != 0; }
-};
-
-// Global allocator shared by all split partitions.
-static TaskIdAllocator gSplitTaskIds;
-
 static bool splitOptAndCodeGenThin(unsigned task, const Config &C,
                                    TargetMachine *TM, AddStreamFn AddStream,
                                    unsigned ParallelCodeGenParallelismLevel,
@@ -610,39 +540,15 @@ static bool splitOptAndCodeGenThin(unsigned task, const Config &C,
                                    ArrayRef<StringRef> &BitcodeLibFuncs) {
   const Target *T = &TM->getTarget();
 
-  static std::mutex PrintMutex;
-
   SplitModuleCG SplitModuleCG(Mod, CombinedIndex, ParallelCodeGenParallelismLevel);
   ParallelCodeGenParallelismLevel = SplitModuleCG.getPartitionNum();
 
-  std::vector<std::string> TempObjectFiles(ParallelCodeGenParallelismLevel);
-  std::vector<llvm::FileRemover> TempFileRemovers(ParallelCodeGenParallelismLevel);
-
   const auto HandleModulePartition = [&](std::unique_ptr<Module> MPart,
                                          unsigned PartitionId) {
-    unsigned CurrentThreadId, UniqueTaskId;
-    {
-      std::lock_guard<std::mutex> Lock(PrintMutex);
-      CurrentThreadId = ThreadCount++;
-
-      // In distributed ThinLTO, `task` may be a sentinel (e.g. -1 cast to
-      // unsigned), which becomes UINT_MAX and naturally has MSB==1. Treat it
-      // as "no base task id" and don't enforce the namespace check on it.
-      //
-      // We do not rely on the incoming `task` for partition uniqueness: split
-      // partitions get a dedicated UniqueTaskId allocated below.
-      if (task != std::numeric_limits<unsigned>::max()) {
-        assert(!TaskIdAllocator::isPartition(task) &&
-               "Original ThinLTO TaskId unexpectedly overlaps the partition "
-               "namespace");
-      }
-      UniqueTaskId = gSplitTaskIds.alloc();
-    }
-
     std::unique_ptr<TargetMachine> ThreadTM = createTargetMachine(C, T, *MPart);
 
     if (DoOpt) {
-      if (!opt(C, ThreadTM.get(), UniqueTaskId, *MPart, /*IsThinLTO=*/true,
+      if (!opt(C, ThreadTM.get(), PartitionId, *MPart, /*IsThinLTO=*/true,
                /*ExportSummary=*/nullptr, /*ImportSummary=*/&CombinedIndex,
                CmdArgs, BitcodeLibFuncs)) {
         report_fatal_error("Failed to gen opt for split mod in thread.");
@@ -653,7 +559,7 @@ static bool splitOptAndCodeGenThin(unsigned task, const Config &C,
       // running `opt()`. We're not reaching here as it's bailed out earlier
       // with `CodeGenOnly` which has been set in `SecondRoundThinBackend`.
       if (IRAddStream)
-        cgdata::saveModuleForTwoRounds(*MPart, task + CurrentThreadId,
+        cgdata::saveModuleForTwoRounds(*MPart, PartitionId,
                                        IRAddStream);
     }
     
@@ -667,91 +573,18 @@ static bool splitOptAndCodeGenThin(unsigned task, const Config &C,
       }
     }
 
-    auto splitStream = [&](unsigned task, const Twine &moduleName)
-        -> Expected<std::unique_ptr<CachedFileStream>> {
-      int FD;
-      SmallString<128> TempFilename;
-      if (std::error_code EC = sys::fs::createTemporaryFile(
-              "thinlto-split", "o", FD, TempFilename))
-        return errorCodeToError(EC);
-
-      TempObjectFiles[PartitionId] = std::string(TempFilename.str());
-      TempFileRemovers[PartitionId].setFile(TempObjectFiles[PartitionId]);
-
-      auto OS = std::make_unique<raw_fd_ostream>(
-          FD, true, /*CloseOnDestruct*/true);
-
-      auto Stream = std::make_unique<CachedFileStream>(
-          std::move(OS), std::string(TempFilename.str()));
-
-      return std::move(Stream);
-    };
-
-    codegen(C, ThreadTM.get(), splitStream, UniqueTaskId, *MPart,
+    // FIXME: For distributed ThinLTO, the current 'Addstream' callbcak needs
+    // to be reconstructed to support emitting multiple split submodules.
+    codegen(C, ThreadTM.get(), AddStream, PartitionId, *MPart,
             CombinedIndex);
   };
 
   SplitModuleCG.SplitModule(HandleModulePartition, C);
 
-  // Use ld.lld to combine the partitions into a object.
-  if (TempObjectFiles.empty()) {
-    llvm::errs() << "TempObjectFiles.empty()\n";
-    return true;
-  }
-
-  auto FinalStream = AddStream(task, Mod.getModuleIdentifier());
-  if (!FinalStream)
-    report_fatal_error("Failed to open final output stream");
-
-  int MergedFD;
-  SmallString<128> MergedFilename;
-  if (sys::fs::createTemporaryFile("thinlto-merged", "o", MergedFD,
-                                   MergedFilename))
-    report_fatal_error("Failed to create merged temp file.");
-  llvm::FileRemover MergedFileRemover(MergedFilename);
-  sys::fs::closeFile(MergedFD);
-
-  std::vector<StringRef> Args;
-  std::string LinkerPath = "";
-  if (auto Path = sys::findProgramByName("ld.lld"))
-    LinkerPath = *Path;
-  else if (auto Path = sys::findProgramByName("ld"))
-    LinkerPath = *Path;
-
-  if (LinkerPath.empty())
-    report_fatal_error("Cannot find linkeer (ld or ld.lld) to merge partitions.");
-
-  Args.push_back(LinkerPath);
-  Args.push_back("-r");
-  Args.push_back("-o");
-  Args.push_back(MergedFilename);
-
-  for (const auto &File : TempObjectFiles)
-    Args.push_back(File);
-
-  std::string ErrMsg;
-  int Result = sys::ExecuteAndWait(LinkerPath, Args, /*Env=*/std::nullopt,
-                                   /*Redirects=*/{}, /*SecondsToWait=*/0,
-                                   /*MemoryLimit=*/0, &ErrMsg);
-
-  if (Result != 0) {
-    errs() << "Linker failed: " << ErrMsg << "\n";
-    report_fatal_error("Failed to merge split objects.");
-  }
+  // TODO: After CodeGen emission, an arbitrary number of split submodules will
+  // be generated. These fragments need to be merged before the final link
+  // stage to prevent disruptions to the distrubuted ThinLTO workflow.
 
-  {
-    std::unique_ptr<CachedFileStream> &FinalFileStream = *FinalStream;
-    auto BufferOrErr = MemoryBuffer::getFile(MergedFilename);
-    if (!BufferOrErr)
-      report_fatal_error("Failed to read merged object.");
-
-    FinalFileStream->OS->write(BufferOrErr.get()->getBufferStart(),
-                               BufferOrErr.get()->getBufferSize());
-    if (Error Err = FinalFileStream->commit()) {
-      report_fatal_error(Twine("Failed to commit final file stream: ") +
-                         toString(std::move(Err)));
-    }
-  }
   return true;
 }
 
@@ -919,21 +752,9 @@ Error lto::thinBackend(const Config &Conf, unsigned Task, AddStreamFn AddStream,
   // the module, if applicable.
   Mod.setPartialSampleProfileRatio(CombinedIndex);
 
-  bool ProfitableToSplit = true;
-  if (ThinLTOSplit) {
-    if (!canDoSplitModule(Mod) || !HasLargeCG(Mod, CombinedIndex)) {
-      ProfitableToSplit = false;
-      LLVM_DEBUG(dbgs() << "warning: thinlto split not enable for module: "
-                        << Mod.getName());
-    } else {
-      LLVM_DEBUG(dbgs() << "thinlto: split codegen for module: "
-                        << Mod.getName());
-    }
-  }
-
   LLVM_DEBUG(dbgs() << "Running ThinLTO\n");
   if (CodeGenOnly) {
-    if (ThinLTOSplit && ProfitableToSplit)
+    if (ThinLTOSplit)
       splitOptAndCodeGenThin(Task, Conf, TM.get(), AddStream,
                              ThinLTOSplitPartitions, Mod, CombinedIndex,
                              CmdArgs, false, IRAddStream, BitcodeLibFuncs);
@@ -950,7 +771,7 @@ Error lto::thinBackend(const Config &Conf, unsigned Task, AddStreamFn AddStream,
   auto OptimizeAndCodegen =
       [&](Module &Mod, TargetMachine *TM,
           LLVMRemarkFileHandle DiagnosticOutputFile) {
-        if (ThinLTOSplit && ProfitableToSplit) {
+        if (ThinLTOSplit) {
           if (!splitOptAndCodeGenThin(
                   Task, Conf, TM, AddStream, ThinLTOSplitPartitions, Mod,
                   CombinedIndex, CmdArgs, true, IRAddStream, BitcodeLibFuncs))
diff --git a/llvm/test/Transforms/SplitModuleCG/split-promoted-rename.ll b/llvm/test/Transforms/SplitModuleCG/split-promoted-rename.ll
deleted file mode 100644
index 6c51141a9ad85..0000000000000
--- a/llvm/test/Transforms/SplitModuleCG/split-promoted-rename.ll
+++ /dev/null
@@ -1,41 +0,0 @@
-; Test that internal symbols promoted during module splitting are consistently
-; renamed with an MD5 suffix across all partitions.
-;
-; RUN: opt -module-summary %s -o %t.bc
-; RUN: llvm-lto2 run %t.bc -o %t \
-; RUN:   -thinlto-split=true \
-; RUN:   -thinlto-split-partitions=2 -thinlto-split-module-size-threshold=0 \
-; RUN:   -r=%t.bc,caller_a,px \
-; RUN:   -r=%t.bc,caller_b,px
-; RUN: llvm-nm %t.1 | FileCheck %s
-
-; CHECK-DAG: T caller_a
-; CHECK-DAG: T caller_b
-; CHECK:     T {{.*promoted_internal[._][0-9a-f]+.*}}
-; CHECK-NOT: T promoted_internal{{$}}
-
-target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
-target triple = "x86_64-unknown-linux-gnu"
-
-; @promoted_internal is internal. SplitModuleCG::dealWithMpart's checkPromoted
-; records it in PromotedRenames. splitOptAndCodeGenThin applies the rename
-; after opt via:
-;   for (auto &GV : MPart->global_values())
-;     if (auto It = PromotedRenames.find(GV.getName()); ...)
-;       GV.setName(It->second);
-define internal void @promoted_internal() {
-entry:
-  ret void
-}
-
-define void @caller_a() {
-entry:
-  call void @promoted_internal()
-  ret void
-}
-
-define void @caller_b() {
-entry:
-  call void @promoted_internal()
-  ret void
-}

>From 0d13949e4e661c82bd8be9bdf28e73789df35725 Mon Sep 17 00:00:00 2001
From: maojiaping <maojiaping1 at huawei.com>
Date: Thu, 11 Jun 2026 10:15:49 +0800
Subject: [PATCH 06/13] [LTO][SplitModuleCG] Enable split module by callgragh
 for FullLTO

- Rename ThinLTOSplit to LTOSplitByCG for clarity
- Add IsThinLTO parameter to splitOptAndCodeGenThin with default true
- Enable splitOptAndCodeGenThin for FullLTO via else if branch
---
 .../thinlto-split/fulllto-split-module.c      | 26 +++++++++++
 .../thinlto-split/thinlto-split-module.c      | 34 ++++++++++++++
 llvm/lib/LTO/LTOBackend.cpp                   | 46 +++++++++++--------
 3 files changed, 87 insertions(+), 19 deletions(-)
 create mode 100644 clang/test/CodeGen/thinlto-split/fulllto-split-module.c
 create mode 100644 clang/test/CodeGen/thinlto-split/thinlto-split-module.c

diff --git a/clang/test/CodeGen/thinlto-split/fulllto-split-module.c b/clang/test/CodeGen/thinlto-split/fulllto-split-module.c
new file mode 100644
index 0000000000000..b3cf7081ee2e0
--- /dev/null
+++ b/clang/test/CodeGen/thinlto-split/fulllto-split-module.c
@@ -0,0 +1,26 @@
+// UNSUPPORTED: system-windows
+// REQUIRES: aarch64-registered-target
+
+// RUN: %clang -flto=full -fuse-ld=lld -shared \
+// RUN:   -o %t.o %s \
+// RUN:   -Wl,-mllvm,-lto-split-by-callgraph=true \
+// RUN:   -Wl,--lto-partitions=2 \
+// RUN:   -Wl,--save-temps=prelink
+// RUN: llvm-nm %t.o.lto.o | FileCheck %s --check-prefix=CHECK0
+// RUN: llvm-nm %t.o.lto.1.o | FileCheck %s --check-prefix=CHECK1
+
+// CHECK0-DAG: T caller_b
+// CHECK0-DAG: T promoted_internal
+
+// CHECK1-DAG: T caller_a
+// CHECK1-DAG: U promoted_internal
+
+static void promoted_internal(void) {}
+
+void caller_a(void) {
+    promoted_internal();
+}
+
+void caller_b(void) {
+    promoted_internal();
+}
\ No newline at end of file
diff --git a/clang/test/CodeGen/thinlto-split/thinlto-split-module.c b/clang/test/CodeGen/thinlto-split/thinlto-split-module.c
new file mode 100644
index 0000000000000..0725fe49f3e6c
--- /dev/null
+++ b/clang/test/CodeGen/thinlto-split/thinlto-split-module.c
@@ -0,0 +1,34 @@
+// UNSUPPORTED: system-windows
+// REQUIRES: aarch64-registered-target
+
+// Distributed ThinLTO (DTLTO)
+// RUN: %clang -flto=thin -c %s -o %t.o
+// RUN: %clang -flto=thin -fuse-ld=lld -Wl,--thinlto-index-only %t.o
+// RUN: not --crash %clang %t.o -c -fthinlto-index=%t.o.thinlto.bc \
+// RUN:                            -mllvm -lto-split-by-callgraph=true \
+// RUN:                            -mllvm -lto-split-partitions=2
+//
+// Regular ThinLTO
+// RUN: %clang -flto=thin -fuse-ld=lld -shared \
+// RUN:   -o %t.o %s \
+// RUN:   -Wl,-mllvm,-lto-split-by-callgraph=true \
+// RUN:   -Wl,-mllvm,-lto-split-partitions=2 \
+// RUN:   -Wl,--save-temps=prelink
+// RUN: llvm-nm %t.o.lto.o | FileCheck %s --check-prefix=CHECK0
+// RUN: llvm-nm %t.o.lto.1.o | FileCheck %s --check-prefix=CHECK1
+
+// CHECK0-DAG: T caller_b
+// CHECK0-DAG: T {{promoted_internal[.][0-9a-f]+}}
+
+// CHECK1-DAG: T caller_a
+// CHECK1-DAG: U {{promoted_internal[.][0-9a-f]+}}
+
+static void promoted_internal(void) {}
+
+void caller_a(void) {
+    promoted_internal();
+}
+
+void caller_b(void) {
+    promoted_internal();
+}
\ No newline at end of file
diff --git a/llvm/lib/LTO/LTOBackend.cpp b/llvm/lib/LTO/LTOBackend.cpp
index 725328d15b537..c69e41af87f38 100644
--- a/llvm/lib/LTO/LTOBackend.cpp
+++ b/llvm/lib/LTO/LTOBackend.cpp
@@ -84,12 +84,12 @@ static cl::list<std::string>
                              "path matches this for -save-temps options"),
                     cl::CommaSeparated, cl::Hidden);
 
-static cl::opt<unsigned> ThinLTOSplitPartitions(
-    "thinlto-split-partitions", cl::Hidden, cl::init(0),
-    cl::desc("Control split to how many partitions in thinlto backend."));
+static cl::opt<unsigned> LTOSplitPartitions(
+    "lto-split-partitions", cl::Hidden, cl::init(0),
+    cl::desc("Control split to how many partitions in lto backend."));
 
-static cl::opt<bool> ThinLTOSplit("thinlto-split", cl::init(false),
-			   cl::desc("Enable split module in thinlto backend."));
+static cl::opt<bool> LTOSplitByCG("lto-split-by-callgraph", cl::init(false),
+			   cl::desc("Enable split module in lto backend."));
 
 namespace llvm {
 extern cl::opt<bool> NoPGOWarnMismatch;
@@ -146,7 +146,7 @@ Error Config::addSaveTemps(std::string OutputFileName, bool UseInputModulePath,
       // named from the provided OutputFileName with the Task ID appended.
       if (M.getModuleIdentifier() == "ld-temp.o" || !UseInputModulePath) {
         PathPrefix = OutputFileName;
-        if (ThinLTOSplit)
+        if (LTOSplitByCG)
           PathPrefix += extract_filename(M.getSourceFileName()) + ".";
         if (Task != (unsigned)-1)
           PathPrefix += utostr(Task) + ".";
@@ -537,7 +537,8 @@ static bool splitOptAndCodeGenThin(unsigned task, const Config &C,
                                    const ModuleSummaryIndex &CombinedIndex,
                                    const std::vector<uint8_t> &CmdArgs,
                                    bool DoOpt, AddStreamFn IRAddStream,
-                                   ArrayRef<StringRef> &BitcodeLibFuncs) {
+                                   ArrayRef<StringRef> &BitcodeLibFuncs,
+                                   bool IsThinLTO = true) {
   const Target *T = &TM->getTarget();
 
   SplitModuleCG SplitModuleCG(Mod, CombinedIndex, ParallelCodeGenParallelismLevel);
@@ -562,14 +563,16 @@ static bool splitOptAndCodeGenThin(unsigned task, const Config &C,
         cgdata::saveModuleForTwoRounds(*MPart, PartitionId,
                                        IRAddStream);
     }
-    
-    // Rename the GlobalValues whose internal is changed to external. That's
-    // can avoid duplicate symbols.
-    auto PromotedRenames = SplitModuleCG.getPromotedRenames();
-    for (auto &GV : MPart->global_values()) {
-      if (auto It = PromotedRenames.find(GV.getName());
-          It != PromotedRenames.end()) {
-        GV.setName(It->second);
+
+    if (IsThinLTO) {
+      // Rename the GlobalValues whose internal is changed to external. That's
+      // can avoid duplicate symbols int ThinLTO.
+      auto PromotedRenames = SplitModuleCG.getPromotedRenames();
+      for (auto &GV : MPart->global_values()) {
+        if (auto It = PromotedRenames.find(GV.getName());
+            It != PromotedRenames.end()) {
+          GV.setName(It->second);
+        }
       }
     }
 
@@ -689,6 +692,11 @@ Error lto::backend(const Config &C, AddStreamFn AddStream,
 
   if (ParallelCodeGenParallelismLevel == 1) {
     codegen(C, TM.get(), AddStream, 0, Mod, CombinedIndex);
+  } else if (LTOSplitByCG) {
+    splitOptAndCodeGenThin(/*Task*/0, C, TM.get(), AddStream,
+                           ParallelCodeGenParallelismLevel, Mod, CombinedIndex,
+                           /*CmdArgs*/ std::vector<uint8_t>(), /*DoOpt*/false,
+                            AddStreamFn(), BitcodeLibFuncs, false);
   } else {
     splitCodeGen(C, TM.get(), AddStream, ParallelCodeGenParallelismLevel, Mod,
                  CombinedIndex);
@@ -754,9 +762,9 @@ Error lto::thinBackend(const Config &Conf, unsigned Task, AddStreamFn AddStream,
 
   LLVM_DEBUG(dbgs() << "Running ThinLTO\n");
   if (CodeGenOnly) {
-    if (ThinLTOSplit)
+    if (LTOSplitByCG)
       splitOptAndCodeGenThin(Task, Conf, TM.get(), AddStream,
-                             ThinLTOSplitPartitions, Mod, CombinedIndex,
+                             LTOSplitPartitions, Mod, CombinedIndex,
                              CmdArgs, false, IRAddStream, BitcodeLibFuncs);
     else
       // If CodeGenOnly is set, we only perform code generation and skip
@@ -771,9 +779,9 @@ Error lto::thinBackend(const Config &Conf, unsigned Task, AddStreamFn AddStream,
   auto OptimizeAndCodegen =
       [&](Module &Mod, TargetMachine *TM,
           LLVMRemarkFileHandle DiagnosticOutputFile) {
-        if (ThinLTOSplit) {
+        if (LTOSplitByCG) {
           if (!splitOptAndCodeGenThin(
-                  Task, Conf, TM, AddStream, ThinLTOSplitPartitions, Mod,
+                  Task, Conf, TM, AddStream, LTOSplitPartitions, Mod,
                   CombinedIndex, CmdArgs, true, IRAddStream, BitcodeLibFuncs))
             return finalizeOptimizationRemarks(std::move(DiagnosticOutputFile));
         } else {

>From ea7b552ab6530d32b28ae529f1c8f815eb1a053e Mon Sep 17 00:00:00 2001
From: maojiaping <maojiaping1 at huawei.com>
Date: Mon, 3 Aug 2026 10:22:08 +0800
Subject: [PATCH 07/13] [SplitModuleCG] Split PR: revert LTO backend changes

split the original PR into two: (1) the SplitModuleCG utility + llvm-split support + associated tests, and (2) the LTO backend integration.

This commit reverts the LTO backend changes (LTOBackend.cpp modifications
and clang-side tests under clang/test/CodeGen/thinlto-split/) so the
Utils-only changes remain. The LTO backend integration will be submitted
in a follow-up patch on top of this branch.
---
 .../thinlto-split/fulllto-split-module.c      |  26 ----
 .../thinlto-split/thinlto-split-module.c      |  34 -----
 llvm/lib/LTO/LTOBackend.cpp                   | 130 +++---------------
 3 files changed, 17 insertions(+), 173 deletions(-)
 delete mode 100644 clang/test/CodeGen/thinlto-split/fulllto-split-module.c
 delete mode 100644 clang/test/CodeGen/thinlto-split/thinlto-split-module.c

diff --git a/clang/test/CodeGen/thinlto-split/fulllto-split-module.c b/clang/test/CodeGen/thinlto-split/fulllto-split-module.c
deleted file mode 100644
index b3cf7081ee2e0..0000000000000
--- a/clang/test/CodeGen/thinlto-split/fulllto-split-module.c
+++ /dev/null
@@ -1,26 +0,0 @@
-// UNSUPPORTED: system-windows
-// REQUIRES: aarch64-registered-target
-
-// RUN: %clang -flto=full -fuse-ld=lld -shared \
-// RUN:   -o %t.o %s \
-// RUN:   -Wl,-mllvm,-lto-split-by-callgraph=true \
-// RUN:   -Wl,--lto-partitions=2 \
-// RUN:   -Wl,--save-temps=prelink
-// RUN: llvm-nm %t.o.lto.o | FileCheck %s --check-prefix=CHECK0
-// RUN: llvm-nm %t.o.lto.1.o | FileCheck %s --check-prefix=CHECK1
-
-// CHECK0-DAG: T caller_b
-// CHECK0-DAG: T promoted_internal
-
-// CHECK1-DAG: T caller_a
-// CHECK1-DAG: U promoted_internal
-
-static void promoted_internal(void) {}
-
-void caller_a(void) {
-    promoted_internal();
-}
-
-void caller_b(void) {
-    promoted_internal();
-}
\ No newline at end of file
diff --git a/clang/test/CodeGen/thinlto-split/thinlto-split-module.c b/clang/test/CodeGen/thinlto-split/thinlto-split-module.c
deleted file mode 100644
index 0725fe49f3e6c..0000000000000
--- a/clang/test/CodeGen/thinlto-split/thinlto-split-module.c
+++ /dev/null
@@ -1,34 +0,0 @@
-// UNSUPPORTED: system-windows
-// REQUIRES: aarch64-registered-target
-
-// Distributed ThinLTO (DTLTO)
-// RUN: %clang -flto=thin -c %s -o %t.o
-// RUN: %clang -flto=thin -fuse-ld=lld -Wl,--thinlto-index-only %t.o
-// RUN: not --crash %clang %t.o -c -fthinlto-index=%t.o.thinlto.bc \
-// RUN:                            -mllvm -lto-split-by-callgraph=true \
-// RUN:                            -mllvm -lto-split-partitions=2
-//
-// Regular ThinLTO
-// RUN: %clang -flto=thin -fuse-ld=lld -shared \
-// RUN:   -o %t.o %s \
-// RUN:   -Wl,-mllvm,-lto-split-by-callgraph=true \
-// RUN:   -Wl,-mllvm,-lto-split-partitions=2 \
-// RUN:   -Wl,--save-temps=prelink
-// RUN: llvm-nm %t.o.lto.o | FileCheck %s --check-prefix=CHECK0
-// RUN: llvm-nm %t.o.lto.1.o | FileCheck %s --check-prefix=CHECK1
-
-// CHECK0-DAG: T caller_b
-// CHECK0-DAG: T {{promoted_internal[.][0-9a-f]+}}
-
-// CHECK1-DAG: T caller_a
-// CHECK1-DAG: U {{promoted_internal[.][0-9a-f]+}}
-
-static void promoted_internal(void) {}
-
-void caller_a(void) {
-    promoted_internal();
-}
-
-void caller_b(void) {
-    promoted_internal();
-}
\ No newline at end of file
diff --git a/llvm/lib/LTO/LTOBackend.cpp b/llvm/lib/LTO/LTOBackend.cpp
index c69e41af87f38..11c9b2f61988b 100644
--- a/llvm/lib/LTO/LTOBackend.cpp
+++ b/llvm/lib/LTO/LTOBackend.cpp
@@ -34,10 +34,8 @@
 #include "llvm/Plugins/PassPlugin.h"
 #include "llvm/Support/Error.h"
 #include "llvm/Support/FileSystem.h"
-#include "llvm/Support/FileUtilities.h"
 #include "llvm/Support/MemoryBuffer.h"
 #include "llvm/Support/Path.h"
-#include "llvm/Support/Program.h"
 #include "llvm/Support/ThreadPool.h"
 #include "llvm/Support/ToolOutputFile.h"
 #include "llvm/Support/VirtualFileSystem.h"
@@ -47,8 +45,6 @@
 #include "llvm/Transforms/IPO/WholeProgramDevirt.h"
 #include "llvm/Transforms/Utils/FunctionImportUtils.h"
 #include "llvm/Transforms/Utils/SplitModule.h"
-#include "llvm/Transforms/Utils/SplitModuleCG.h"
-#include <filesystem>
 #include <optional>
 
 using namespace llvm;
@@ -84,13 +80,6 @@ static cl::list<std::string>
                              "path matches this for -save-temps options"),
                     cl::CommaSeparated, cl::Hidden);
 
-static cl::opt<unsigned> LTOSplitPartitions(
-    "lto-split-partitions", cl::Hidden, cl::init(0),
-    cl::desc("Control split to how many partitions in lto backend."));
-
-static cl::opt<bool> LTOSplitByCG("lto-split-by-callgraph", cl::init(false),
-			   cl::desc("Enable split module in lto backend."));
-
 namespace llvm {
 extern cl::opt<bool> NoPGOWarnMismatch;
 }
@@ -135,19 +124,12 @@ Error Config::addSaveTemps(std::string OutputFileName, bool UseInputModulePath,
       if (LinkerHook && !LinkerHook(Task, M))
         return false;
 
-      auto extract_filename = [](const std::string &path) -> std::string {
-        std::filesystem::path fs_path(path);
-        return fs_path.filename().string();
-      };
-
       std::string PathPrefix;
       // If this is the combined module (not a ThinLTO backend compile) or the
       // user hasn't requested using the input module's path, emit to a file
       // named from the provided OutputFileName with the Task ID appended.
       if (M.getModuleIdentifier() == "ld-temp.o" || !UseInputModulePath) {
         PathPrefix = OutputFileName;
-        if (LTOSplitByCG)
-          PathPrefix += extract_filename(M.getSourceFileName()) + ".";
         if (Task != (unsigned)-1)
           PathPrefix += utostr(Task) + ".";
       } else
@@ -530,67 +512,6 @@ static void codegen(const Config &Conf, TargetMachine *TM,
     report_fatal_error(std::move(Err));
 }
 
-static bool splitOptAndCodeGenThin(unsigned task, const Config &C,
-                                   TargetMachine *TM, AddStreamFn AddStream,
-                                   unsigned ParallelCodeGenParallelismLevel,
-                                   Module &Mod,
-                                   const ModuleSummaryIndex &CombinedIndex,
-                                   const std::vector<uint8_t> &CmdArgs,
-                                   bool DoOpt, AddStreamFn IRAddStream,
-                                   ArrayRef<StringRef> &BitcodeLibFuncs,
-                                   bool IsThinLTO = true) {
-  const Target *T = &TM->getTarget();
-
-  SplitModuleCG SplitModuleCG(Mod, CombinedIndex, ParallelCodeGenParallelismLevel);
-  ParallelCodeGenParallelismLevel = SplitModuleCG.getPartitionNum();
-
-  const auto HandleModulePartition = [&](std::unique_ptr<Module> MPart,
-                                         unsigned PartitionId) {
-    std::unique_ptr<TargetMachine> ThreadTM = createTargetMachine(C, T, *MPart);
-
-    if (DoOpt) {
-      if (!opt(C, ThreadTM.get(), PartitionId, *MPart, /*IsThinLTO=*/true,
-               /*ExportSummary=*/nullptr, /*ImportSummary=*/&CombinedIndex,
-               CmdArgs, BitcodeLibFuncs)) {
-        report_fatal_error("Failed to gen opt for split mod in thread.");
-      }
-
-      // Save the current module before the first codegen round.
-      // Note that the second codegen round runs only `codegen()` without
-      // running `opt()`. We're not reaching here as it's bailed out earlier
-      // with `CodeGenOnly` which has been set in `SecondRoundThinBackend`.
-      if (IRAddStream)
-        cgdata::saveModuleForTwoRounds(*MPart, PartitionId,
-                                       IRAddStream);
-    }
-
-    if (IsThinLTO) {
-      // Rename the GlobalValues whose internal is changed to external. That's
-      // can avoid duplicate symbols int ThinLTO.
-      auto PromotedRenames = SplitModuleCG.getPromotedRenames();
-      for (auto &GV : MPart->global_values()) {
-        if (auto It = PromotedRenames.find(GV.getName());
-            It != PromotedRenames.end()) {
-          GV.setName(It->second);
-        }
-      }
-    }
-
-    // FIXME: For distributed ThinLTO, the current 'Addstream' callbcak needs
-    // to be reconstructed to support emitting multiple split submodules.
-    codegen(C, ThreadTM.get(), AddStream, PartitionId, *MPart,
-            CombinedIndex);
-  };
-
-  SplitModuleCG.SplitModule(HandleModulePartition, C);
-
-  // TODO: After CodeGen emission, an arbitrary number of split submodules will
-  // be generated. These fragments need to be merged before the final link
-  // stage to prevent disruptions to the distrubuted ThinLTO workflow.
-
-  return true;
-}
-
 static void splitCodeGen(const Config &C, TargetMachine *TM,
                          AddStreamFn AddStream,
                          unsigned ParallelCodeGenParallelismLevel, Module &Mod,
@@ -692,11 +613,6 @@ Error lto::backend(const Config &C, AddStreamFn AddStream,
 
   if (ParallelCodeGenParallelismLevel == 1) {
     codegen(C, TM.get(), AddStream, 0, Mod, CombinedIndex);
-  } else if (LTOSplitByCG) {
-    splitOptAndCodeGenThin(/*Task*/0, C, TM.get(), AddStream,
-                           ParallelCodeGenParallelismLevel, Mod, CombinedIndex,
-                           /*CmdArgs*/ std::vector<uint8_t>(), /*DoOpt*/false,
-                            AddStreamFn(), BitcodeLibFuncs, false);
   } else {
     splitCodeGen(C, TM.get(), AddStream, ParallelCodeGenParallelismLevel, Mod,
                  CombinedIndex);
@@ -762,14 +678,9 @@ Error lto::thinBackend(const Config &Conf, unsigned Task, AddStreamFn AddStream,
 
   LLVM_DEBUG(dbgs() << "Running ThinLTO\n");
   if (CodeGenOnly) {
-    if (LTOSplitByCG)
-      splitOptAndCodeGenThin(Task, Conf, TM.get(), AddStream,
-                             LTOSplitPartitions, Mod, CombinedIndex,
-                             CmdArgs, false, IRAddStream, BitcodeLibFuncs);
-    else
-      // If CodeGenOnly is set, we only perform code generation and skip
-      // optimization. This value may differ from Conf.CodeGenOnly.
-      codegen(Conf, TM.get(), AddStream, Task, Mod, CombinedIndex);
+    // If CodeGenOnly is set, we only perform code generation and skip
+    // optimization. This value may differ from Conf.CodeGenOnly.
+    codegen(Conf, TM.get(), AddStream, Task, Mod, CombinedIndex);
     return finalizeOptimizationRemarks(std::move(DiagnosticOutputFile));
   }
 
@@ -779,27 +690,20 @@ Error lto::thinBackend(const Config &Conf, unsigned Task, AddStreamFn AddStream,
   auto OptimizeAndCodegen =
       [&](Module &Mod, TargetMachine *TM,
           LLVMRemarkFileHandle DiagnosticOutputFile) {
-        if (LTOSplitByCG) {
-          if (!splitOptAndCodeGenThin(
-                  Task, Conf, TM, AddStream, LTOSplitPartitions, Mod,
-                  CombinedIndex, CmdArgs, true, IRAddStream, BitcodeLibFuncs))
-            return finalizeOptimizationRemarks(std::move(DiagnosticOutputFile));
-        } else {
-          // Perform optimization and code generation for ThinLTO.
-          if (!opt(Conf, TM, Task, Mod, /*IsThinLTO=*/true,
-                  /*ExportSummary=*/nullptr, /*ImportSummary=*/&CombinedIndex,
-                  CmdArgs, BitcodeLibFuncs))
-            return finalizeOptimizationRemarks(std::move(DiagnosticOutputFile));
-
-          // Save the current module before the first codegen round.
-          // Note that the second codegen round runs only `codegen()` without
-          // running `opt()`. We're not reaching here as it's bailed out earlier
-          // with `CodeGenOnly` which has been set in `SecondRoundThinBackend`.
-          if (IRAddStream)
-            cgdata::saveModuleForTwoRounds(Mod, Task, IRAddStream);
-
-          codegen(Conf, TM, AddStream, Task, Mod, CombinedIndex);
-        }
+        // Perform optimization and code generation for ThinLTO.
+        if (!opt(Conf, TM, Task, Mod, /*IsThinLTO=*/true,
+                 /*ExportSummary=*/nullptr, /*ImportSummary=*/&CombinedIndex,
+                 CmdArgs, BitcodeLibFuncs))
+          return finalizeOptimizationRemarks(std::move(DiagnosticOutputFile));
+
+        // Save the current module before the first codegen round.
+        // Note that the second codegen round runs only `codegen()` without
+        // running `opt()`. We're not reaching here as it's bailed out earlier
+        // with `CodeGenOnly` which has been set in `SecondRoundThinBackend`.
+        if (IRAddStream)
+          cgdata::saveModuleForTwoRounds(Mod, Task, IRAddStream);
+
+        codegen(Conf, TM, AddStream, Task, Mod, CombinedIndex);
         return finalizeOptimizationRemarks(std::move(DiagnosticOutputFile));
       };
 

>From 0f04df55598daa5965e1ecaf8daa649b54dc3259 Mon Sep 17 00:00:00 2001
From: maojiaping <maojiaping1 at huawei.com>
Date: Mon, 3 Aug 2026 14:38:13 +0800
Subject: [PATCH 08/13] [SplitModuleCG] Address review feedback from PR #198702

- Update partition cost incrementally and use early continue.
- Remove empty if and clarify indirect-call handling.
- Switch SimplifyCallGraph::FunctionMap to DenseMap.
- Use getUniqueModuleId with .llvm.<hash> suffix for promoted locals.
- Remove the unused CombinedIndex parameter from the SimplifyCallGraph
  and SplitModuleCG constructors and createSimplifyCallGraph.
- Add class/function doc comments and document private methods.
- Reword misleading comments in SplitModule.
---
 .../llvm/Transforms/Utils/SplitModuleCG.h     | 109 ++++++++++++++----
 llvm/lib/Transforms/Utils/SplitModuleCG.cpp   |  99 ++++++++--------
 llvm/tools/llvm-split/llvm-split.cpp          |   4 +-
 3 files changed, 137 insertions(+), 75 deletions(-)

diff --git a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
index 9836376b94a82..b6c1b7e3303e5 100644
--- a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
+++ b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
@@ -3,7 +3,6 @@
 
 #include "llvm/ADT/StringSet.h"
 #include "llvm/Analysis/CallGraph.h"
-#include "llvm/Analysis/ModuleSummaryAnalysis.h"
 #include "llvm/LTO/Config.h"
 #include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/DenseSet.h"
@@ -15,19 +14,31 @@ class SimplifyCallGraphNode;
 
 using CostType = InstructionCost::CostType;
 
+/// A simplified view of the LLVM CallGraph used by SplitModuleCG to drive
+/// callgraph-based module partitioning.
+///
+/// SimplifyCallGraph drops the function-instruction-level details that the
+/// full CallGraph carries and keeps only the information needed for
+/// partitioning decisions:
+///   - The set of functions in the module (one SimplifyCallGraphNode each).
+///   - The static call edges between them.
+///   - A reference count (NumReferences) recording how many other functions
+///     call a given function. Functions with a reference count of zero are
+///     treated as call-graph roots during partitioning.
+///
+/// The simplified graph is built once (in createSimplifyCallGraph) and is
+/// consumed by SplitModuleCG::createWorkList to discover roots and their
+/// transitive dependencies.
 class SimplifyCallGraph {
   using FunctionMapTy =
-      std::map<const Function *, std::unique_ptr<SimplifyCallGraphNode>>;
+      DenseMap<const Function *, std::unique_ptr<SimplifyCallGraphNode>>;
 
   /// A map from \c Function* to \c SimplifyCallGraphNode*.
   FunctionMapTy FunctionMap;
 
 public:
-  explicit SimplifyCallGraph(CallGraph &CG,
-                             const ModuleSummaryIndex &CombinedIndex,
-                             Module &M)
-      : CG(CG) {
-    createSimplifyCallGraph(CombinedIndex);
+  explicit SimplifyCallGraph(CallGraph &CG, Module &M) : CG(CG) {
+    createSimplifyCallGraph();
   }
   ~SimplifyCallGraph() {};
 
@@ -68,7 +79,7 @@ class SimplifyCallGraph {
     return I->second.get();
   }
 
-  void createSimplifyCallGraph(const ModuleSummaryIndex &CombinedIndex);
+  void createSimplifyCallGraph();
   void print();
   SimplifyCallGraphNode *getOrInsertFunction(const Function *F);
 
@@ -76,6 +87,9 @@ class SimplifyCallGraph {
   CallGraph &CG;
 };
 
+/// A node in SimplifyCallGraph representing a single function, plus the set
+/// of functions it calls. Provides reference counting so the caller
+/// can identify roots (in-degree 0) during partitioning.
 class SimplifyCallGraphNode {
 public:
   using CalledFunctionsSet = DenseSet<SimplifyCallGraphNode *>;
@@ -135,19 +149,17 @@ static void addAllDependencies(SimplifyCallGraph &SCG, const Function &F,
     const auto &CurFn = *WorkList.pop_back_val();
     assert(!CurFn.isDeclaration());
 
-    // Scan for an indirect call. If such a call is found, we have to
-    // conservatively assume this can call all non-entrypoint functions in 
-    // the module.
+    // Walk the callees of CurFn recorded in SimplifyCallGraph and
+    // add them to Fns, recursing transitively via the WorkList.
     for (auto &SCGNode : *SCG.at(&CurFn)) {
       auto *Callee = SCGNode->getFunction();
       if (!Callee || Callee->isDeclaration())
         continue;
-      if (Callee != &F)
-      {
-        auto [It, Inserted] = Fns.insert(Callee);
-        if (Inserted)
-          WorkList.push_back(Callee);
-      }
+      // Don't recurse into the starting function itself (would re-add F).
+      if (Callee == &F)
+        continue;
+      if (Fns.insert(Callee).second)
+        WorkList.push_back(Callee);
     }
   }
 }
@@ -170,15 +182,44 @@ struct FunctionWithDependencies {
   CostType TotalCost = 0;
 };
 
-/// Splits the module M into N linkable partitions. The function ModuleCallback
-/// is called N times passing each individual partition as the MPart argument.
+/// Splits a module into N linkable partitions by traversing its call graph,
+/// so that each partition carries a self-consistent subset of functions
+/// (a root + its callees) and is balanced by IR-instruction cost. The
+/// resulting partitions can be optimized and codegen'd in parallel by the
+/// LTO backend and merged back into a single object.
+///
+/// Workflow (driven by SplitModule):
+///   1. externalize(): promote local symbols to external+hidden so they are
+///      visible across partitions. Unnamed entities get a stable name.
+///   2. calculateFunctionCosts(): compute per-function IR instruction counts.
+///   3. createWorkList(): walk SimplifyCallGraph to discover call-graph roots
+///      and their transitive dependencies.
+///   4. doPartitioning(): greedily assign each root + dependencies to the
+///      least-loaded partition, balancing by accumulated cost.
+///   5. For each partition: CloneModule the original module filtered by
+///      ShouldCloneDefinition, then dealWithMpart cleans up unused locals,
+///      marks available-externally-defined functions, and records (in
+///      PromotedRenames) the renaming for promoted locals so the caller can
+///      apply it later.
+///   6. Each partition bitcode is serialized to its own LLVMContext (via
+///      write+read) so partitions can be processed on concurrent threads
+///      without sharing LLVMContext state.
 class SplitModuleCG {
 public:
   using ModuleCreationCallback =
       function_ref<void(std::unique_ptr<Module> MPart, unsigned PartitionId)>;
-  SplitModuleCG(Module &M,
-                const ModuleSummaryIndex &CombinedIndex,
-                unsigned LimitPartition = 0);
+
+  /// Construct a SplitModuleCG over module \p M.
+  ///
+  /// \param M The module to partition. Must outlive the SplitModuleCG
+  ///          instance and any partitions emitted via SplitModule().
+  /// \param LimitPartition Upper bound on the number of partitions to
+  ///          produce. Pass 0 (the default) to derive the partition count
+  ///          from the number of call-graph roots discovered in
+  ///          createWorkList (one root per partition at most). The actual
+  ///          partition count is finalized in the constructor and can be
+  ///          queried via getPartitionNum().
+  SplitModuleCG(Module &M, unsigned LimitPartition = 0);
   void SplitModule(ModuleCreationCallback ModuleCallback,
                    const llvm::lto::Config &C);
 
@@ -199,11 +240,35 @@ class SplitModuleCG {
   DenseMap<const Function *, CostType> FuncsCosts;
   SmallVector<FunctionWithDependencies> FWDWorkList;
 
+  /// Compute the IR-instruction cost of every non-declaration function in M
+  /// and populate FuncsCosts / ModuleCost.
   void calculateFunctionCosts();
+
+  /// Walk FWDWorkList in cost-sorted order and greedily assign each root and
+  /// its dependencies to the partition with the lowest accumulated cost
+  /// (load-balanced bin-packing). Returns N partition sets, one per partition.
   std::vector<DenseSet<const Function *>> doPartitioning();
+
+  /// Post-process a cloned partition \p MPart (partition index \p I):
+  ///   - Record promoted names for symbols that were local but
+  ///     are now external (not in OriginalExternals) into PromotedRenames.
+  ///   - Erase conservatively-cloned local globals that ended up with no users.
+  ///   - For functions that were already external in the source module and
+  ///     are being defined in this partition, downgrade their duplicate
+  ///     definitions in other partitions to available_externally via the
+  ///     externalFunction map.
+  /// \p NeedsConservativeImport is the predicate (captured by SplitModule)
+  /// that identifies local globals that must be cloned into every partition.
   void dealWithMpart(
       Module &MPart, unsigned I,
       function_ref<bool(const GlobalValue *)> NeedsConservativeImport);
+
+  /// Discover call-graph roots (functions with in-degree 0 in SCG) and
+  /// build FWDWorkList, where each entry is a root + its transitive
+  /// dependency closure + the total cost. Functions in cycles that no
+  /// root reaches are treated as standalone roots themselves. The list
+  /// is sorted by (TotalCost desc, Name asc) so the most expensive roots
+  /// are assigned first during partitioning.
   void createWorkList();
 };
 
diff --git a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
index c50111204e1f0..16032a3336f1d 100644
--- a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
+++ b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
@@ -7,6 +7,7 @@
 #include "llvm/IR/Value.h"
 #include "llvm/Support/MD5.h"
 #include "llvm/Transforms/Utils/Cloning.h"
+#include "llvm/Transforms/Utils/ModuleUtils.h"
 #include <thread>
 using namespace llvm;
 
@@ -60,21 +61,23 @@ std::vector<DenseSet<const Function *>> SplitModuleCG::doPartitioning() {
   // care of updating the balancing queue.
   const auto AssignToPartition = [&](PartitionID PID,
                                      const FunctionWithDependencies &FWD) {
+    // Insert the root function and its dependencies into the partition,
+    // tracking the cost of newly inserted functions so the balancing queue
+    // can be updated.
     auto &FnsInPart = Partitions[PID];
-    FnsInPart.insert(FWD.F);
-    for (const Function *Dep : FWD.Dependencies) {
-      FnsInPart.insert(Dep);
-    }
-
-    // Update the balancing queue. we scan backwards because in the common case
-    // the partition is at the end.
+    CostType AddedCost = 0;
+    if (FnsInPart.insert(FWD.F).second)
+      AddedCost += FuncsCosts.at(FWD.F);
+    for (const Function *Dep : FWD.Dependencies)
+      if (FnsInPart.insert(Dep).second)
+        AddedCost += FuncsCosts.lookup(Dep);
+
+    // Update the balancing queue. We scan backwards because in the common
+    // case the target partition is at the end of the sorted queue.
     for (auto &[QueuePID, Cost] : reverse(BalancingQueue)) {
-      if (QueuePID == PID) {
-        CostType NewCost = 0;
-        for (auto *Fn : Partitions[PID])
-          NewCost += FuncsCosts.at(Fn);
-        Cost = NewCost;
-      }
+      if (QueuePID != PID)
+        continue;
+      Cost += AddedCost;
     }
 
     sort(BalancingQueue, ComparePartitions);
@@ -108,20 +111,25 @@ void SplitModuleCG::calculateFunctionCosts() {
 }
 
 void SplitModuleCG::dealWithMpart(Module &MPart, unsigned I,
-                                  function_ref<bool(const GlobalValue *)> NeedsConservativeImport) {
-  // collect symbols to rename
+                                   function_ref<bool(const GlobalValue *)> NeedsConservativeImport) {
+  // Collect promoted symbols (those that were local but are now external due
+  // to externalize(), and therefore are not in the OriginalExternals set
+  // captured at construction time).
+  //
+  // Note: here we only *record* the rename in PromotedRenames; we do not
+  // perform the actual renaming immediately. The rename is applied after the
+  // opt pipeline has completed. This is intentional: deferring the rename
+  // minimizes the impact of renaming on subsequent optimizations.
   auto checkPromoted = [&](const GlobalValue &GV) {
     // now is external (not local), but not in external set.
     if (!GV.hasLocalLinkage() && !OriginalExternals.contains(GV.getName())) {
       if (PromotedRenames.count(GV.getName()))
         return;
-      MD5 Hash;
-      Hash.update(M.getModuleIdentifier());
-      MD5::MD5Result Result;
-      Hash.final(Result);
-      SmallString<32> HashStr;
-      MD5::stringifyResult(Result, HashStr);
-      std::string NewName = (GV.getName() + "." + HashStr.str().substr(0, 8)).str();
+      // Use the naming convention "name.llvm.<suffix>" so the
+      // promoted local cannot clash with an external that happens to share
+      // the same name in another module/partition.
+      std::string Suffix = getUniqueModuleId(&M);
+      std::string NewName = (GV.getName() + ".llvm" + Suffix).str();
       PromotedRenames[GV.getName()] = NewName;
     }
   };
@@ -230,18 +238,13 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
   for (GlobalIFunc &GI : M.ifuncs())
     externalize(&GI);
 
-  // TODO: Consider optimizing the alias, replacing the determined alias with
-  // the determined aliasee.
-
   // Assign callgraphs into N partitions.
   auto Partitions = doPartitioning();
   assert(Partitions.size() == N);
 
-  // local GVs need to be conservatively imported into [dependency] every module,
- 	// and then cleaned up afterwards.
   const auto NeedsConservativeImport = [&](const GlobalValue *GV) {
-    // We conservatively import private/internal GVs into every module and clean
-    // them up afterwards.
+    // Conservatively clone private/internal globals into every partition;
+    // unused copies are removed by dealWithMpart afterwards.
     const auto *Var = dyn_cast<GlobalVariable>(GV);
     return Var && Var->hasLocalLinkage();
   };
@@ -260,8 +263,13 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
     return I == 0;
   };
 
-  // TODO: In the future, it may be considered to also include clonemodule in
-  // parallel to reduce compilation time.
+  // TODO: Consider parallelizing the per-partition CloneModule call itself.
+  // Today the loop below serially clones M into N partitions in the main
+  // thread, then spawns N worker threads to run opt+codegen. If CloneModule
+  // becomes a bottleneck for large modules, the clones could be produced in
+  // parallel too — but that would require either per-thread LLVMContexts
+  // for the clone step or a thread-safe CloneModule, neither of which is
+  // straightforward.
   std::vector<std::thread> Threads;
   Threads.reserve(N);
   std::vector<std::unique_ptr<Module>> MPartInCtxs;
@@ -275,9 +283,13 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
 
     dealWithMpart(*MPart, I, NeedsConservativeImport);
 
-    // If not clone module in multi-thread, we also need to clone
-    // the module obtained through segmentation into a new context
-    // to avoid data races.
+    // Serialize the cloned partition to bitcode and re-parse it inside the
+    // worker thread's own LLVMContext. This round-trip is required because
+    // LLVM's Module / LLVMContext are not safe to share across threads:
+    // CloneModule above runs in the main thread's context, but the worker
+    // thread created below needs its own context to run opt + codegen
+    // concurrently without racing on shared internal state. So bitcode
+    // serialization is the supported way to move a Module between contexts.
     SmallString<0> BC;
     raw_svector_ostream BCOS(BC);
     WriteBitcodeToFile(*MPart, BCOS);
@@ -296,9 +308,7 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
     T.join();
 }
 
-SplitModuleCG::SplitModuleCG(Module &M,
-                             const ModuleSummaryIndex &CombinedIndex,
-                             unsigned LimitPartition)
+SplitModuleCG::SplitModuleCG(Module &M, unsigned LimitPartition)
     : N(LimitPartition), M(M), CG(M) {
   // Track existing non-local symbols. This ensures that when we promote
   // internal symbols to external for partitioning, we can handle renaming
@@ -310,9 +320,7 @@ SplitModuleCG::SplitModuleCG(Module &M,
   calculateFunctionCosts();
 
   // Construct a simplified call graph to facilitate worklist generation.
-  SCG = std::make_unique<SimplifyCallGraph>(CG, CombinedIndex, M);
-  // TODO: When the SCG is established, the special cases of comdat and
-  // initarray need to be considered.
+  SCG = std::make_unique<SimplifyCallGraph>(CG, M);
 
   // Populate the worklist with root functions and their transitive
   // dependencies. This worklist serves as the foundation for the
@@ -325,8 +333,7 @@ SplitModuleCG::SplitModuleCG(Module &M,
   N = N == 0 ? 1 : N;
 }
 
-void SimplifyCallGraph::createSimplifyCallGraph(
-    const ModuleSummaryIndex &CombinedIndex) {
+void SimplifyCallGraph::createSimplifyCallGraph() {
   for (auto &NodePair : CG) {
     CallGraphNode *CGNode = NodePair.second.get();
     Function *F = CGNode->getFunction();
@@ -335,16 +342,8 @@ void SimplifyCallGraph::createSimplifyCallGraph(
 
     SimplifyCallGraphNode *SCGNode = getOrInsertFunction(F);
 
-    //TODO: Trace indirect call usage for the current function.
-
     for (const auto &CGNodeItem : *CGNode) {
       Function *Called = CGNodeItem.second->getFunction();
-      if (!Called) {
-        //TODO: Deal with indirect call. 
-        // 1. Check if the instruction has a callees metadata.
-        // 2. Check if this is an indirect call with profile data.
-        // 3. Check if this is an alias to a function.
-      }
       if (!Called || Called->isDeclaration())
         continue;
       SCGNode->addCalledFunction(getOrInsertFunction(Called));
diff --git a/llvm/tools/llvm-split/llvm-split.cpp b/llvm/tools/llvm-split/llvm-split.cpp
index 9294ce1e4c9f1..69c35e5f0d8af 100644
--- a/llvm/tools/llvm-split/llvm-split.cpp
+++ b/llvm/tools/llvm-split/llvm-split.cpp
@@ -18,7 +18,6 @@
 #include "llvm/IR/LLVMContext.h"
 #include "llvm/IR/PassInstrumentation.h"
 #include "llvm/IR/PassManager.h"
-#include "llvm/IR/ModuleSummaryIndex.h"
 #include "llvm/IR/Verifier.h"
 #include "llvm/IRReader/IRReader.h"
 #include "llvm/LTO/Config.h"
@@ -353,8 +352,7 @@ int main(int argc, char **argv) {
     };
 
     llvm::lto::Config Config;
-    ModuleSummaryIndex CombinedIndex(false);
-    SplitModuleCG SplitModuleCG(*M, CombinedIndex, NumOutputs);
+    SplitModuleCG SplitModuleCG(*M, NumOutputs);
     SplitModuleCG.SplitModule(HandleModulePartCG, Config);
     return 0;
   }

>From 332b3780d2e1e4f3c3fa5dd3f8c98163b9907372 Mon Sep 17 00:00:00 2001
From: maojiaping <maojiaping1 at huawei.com>
Date: Sat, 29 Aug 2026 16:32:46 +0800
Subject: [PATCH 09/13] [SplitModuleCG] Address review feedback: cleanup,
 naming, and dead code removal

- Naming: rename SimplifyCallGraph -> SimplifiedCallGraph, ring -> cycle,
  flag enable-split-module-CG -> enable-call-graph-split-module, and
  adopt lower camel case for methods.
- Data structure: switch FunctionMap to std::map for deterministic
  iteration; add values() via make_second_range; deduplicate const/
  non-const at() via const_cast; keep unique_ptr refs instead of .get().
- Dead code: remove unused API (operator[], count(), removeCalledFunction,
  NeedsConservativeClone, MPartInCtxs, PromotedRenames, unused getters),
  redundant name lookups, and single-use wrappers/lambdas.
- Code style: use contains() over count(), early continue, remove braces
  for single-statement bodies, guard debug loops with #ifndef NDEBUG,
  replace runtime checks with assert.
- Promoted rename: move from llvm-split callback into dealWithMpart;
  apply directly via getUniqueModuleId (deterministic, no shared map).
- Documentation: fix/add doc comments for classes, functions, and members.
- Tests: add 1-2 sentence descriptions, rename function-with-ring.ll
  -> function-with-cycle.ll, update checks for .llvm. suffix.
---
 .../llvm/Transforms/Utils/SplitModuleCG.h     | 152 ++++++-------
 llvm/lib/Transforms/Utils/SplitModuleCG.cpp   | 200 ++++++++----------
 ...on-with-ring.ll => function-with-cycle.ll} |   5 +-
 .../llvm-split/SplitModuleCG/function.ll      |   5 +-
 .../llvm-split/SplitModuleCG/partition-cap.ll |   6 +-
 .../SplitModuleCG/promoted-rename.ll          |  23 ++
 .../SplitModuleCG/single-partition.ll         |   5 +-
 .../tools/llvm-split/SplitModuleCG/unnamed.ll |   9 +-
 llvm/tools/llvm-split/llvm-split.cpp          |   8 +-
 9 files changed, 195 insertions(+), 218 deletions(-)
 rename llvm/test/tools/llvm-split/SplitModuleCG/{function-with-ring.ll => function-with-cycle.ll} (75%)
 create mode 100644 llvm/test/tools/llvm-split/SplitModuleCG/promoted-rename.ll

diff --git a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
index b6c1b7e3303e5..4f45fb82fe501 100644
--- a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
+++ b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
@@ -6,179 +6,159 @@
 #include "llvm/LTO/Config.h"
 #include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/DenseSet.h"
+#include <map>
 
 namespace llvm {
 
-class SimplifyCallGraph;
-class SimplifyCallGraphNode;
+class SimplifiedCallGraph;
+class SimplifiedCallGraphNode;
 
 using CostType = InstructionCost::CostType;
 
 /// A simplified view of the LLVM CallGraph used by SplitModuleCG to drive
 /// callgraph-based module partitioning.
 ///
-/// SimplifyCallGraph drops the function-instruction-level details that the
+/// SimplifiedCallGraph drops the function-instruction-level details that the
 /// full CallGraph carries and keeps only the information needed for
 /// partitioning decisions:
-///   - The set of functions in the module (one SimplifyCallGraphNode each).
+///   - The set of functions in the module (one SimplifiedCallGraphNode each).
 ///   - The static call edges between them.
 ///   - A reference count (NumReferences) recording how many other functions
 ///     call a given function. Functions with a reference count of zero are
 ///     treated as call-graph roots during partitioning.
 ///
-/// The simplified graph is built once (in createSimplifyCallGraph) and is
+/// The simplified graph is built once (in the constructor) and is
 /// consumed by SplitModuleCG::createWorkList to discover roots and their
 /// transitive dependencies.
-class SimplifyCallGraph {
+class SimplifiedCallGraph {
   using FunctionMapTy =
-      DenseMap<const Function *, std::unique_ptr<SimplifyCallGraphNode>>;
+      std::map<const Function *, std::unique_ptr<SimplifiedCallGraphNode>>;
 
-  /// A map from \c Function* to \c SimplifyCallGraphNode*.
+  /// A map from \c Function* to \c SimplifiedCallGraphNode*.
   FunctionMapTy FunctionMap;
 
 public:
-  explicit SimplifyCallGraph(CallGraph &CG, Module &M) : CG(CG) {
-    createSimplifyCallGraph();
-  }
-  ~SimplifyCallGraph() {};
+  explicit SimplifiedCallGraph(CallGraph &CG);
+  ~SimplifiedCallGraph() = default;
 
   using iterator = FunctionMapTy::iterator;
   using const_iterator = FunctionMapTy::const_iterator;
 
-  /// Returns the module the call graph corresponds to.
+  /// Iterates over all (Function*, SimplifiedCallGraphNode) pairs in the
+  /// call graph.
   inline iterator begin() { return FunctionMap.begin(); }
   inline iterator end() { return FunctionMap.end(); }
   inline const_iterator begin() const { return FunctionMap.begin(); }
   inline const_iterator end() const { return FunctionMap.end(); }
 
-  /// Returns the call graph node for the provided function.
-  inline const SimplifyCallGraphNode *operator[](const Function *F) const {
-    const_iterator I = FunctionMap.find(F);
-    assert(I != FunctionMap.end() && "Function not in callgraph!");
-    return I->second.get();
-  }
-
-  /// Returns the call graph node for the provided function.
-  inline SimplifyCallGraphNode *operator[](const Function *F) {
-    const_iterator I = FunctionMap.find(F);
-    assert(I != FunctionMap.end() && "Function not in callgraph!");
-    return I->second.get(); 
-  }
+  /// Iterates over all SimplifiedCallGraphNode (unique_ptr) values.
+  auto values() { return llvm::make_second_range(FunctionMap); }
+  auto values() const { return llvm::make_second_range(FunctionMap); }
 
   /// Returns the call graph node for the provided function.
-  inline const SimplifyCallGraphNode *at(const Function *F) const {
+  inline const SimplifiedCallGraphNode *at(const Function *F) const {
     const_iterator I = FunctionMap.find(F);
     assert(I != FunctionMap.end() && "Function not in callgraph!");
     return I->second.get();
   }
 
-  /// Returns the call graph node for the provided function.
-  inline SimplifyCallGraphNode *at(const Function *F) {
-    const_iterator I = FunctionMap.find(F);
-    assert(I != FunctionMap.end() && "Function not in callgraph!");
-    return I->second.get();
+  inline SimplifiedCallGraphNode *at(const Function *F) {
+    return const_cast<SimplifiedCallGraphNode *>(
+        static_cast<const SimplifiedCallGraph &>(*this).at(F));
   }
 
-  void createSimplifyCallGraph();
   void print();
-  SimplifyCallGraphNode *getOrInsertFunction(const Function *F);
-
-private:
-  CallGraph &CG;
+  SimplifiedCallGraphNode *getOrInsertFunction(const Function *F);
 };
 
-/// A node in SimplifyCallGraph representing a single function, plus the set
+/// A node in SimplifiedCallGraph representing a single function, plus the set
 /// of functions it calls. Provides reference counting so the caller
 /// can identify roots (in-degree 0) during partitioning.
-class SimplifyCallGraphNode {
+class SimplifiedCallGraphNode {
 public:
-  using CalledFunctionsSet = DenseSet<SimplifyCallGraphNode *>;
-  inline SimplifyCallGraphNode(SimplifyCallGraph *SCG, Function *F)
+  inline SimplifiedCallGraphNode(Function *F)
       : F(F) {}
 
-  SimplifyCallGraphNode(const SimplifyCallGraphNode &) = delete;
-  SimplifyCallGraphNode &operator=(const SimplifyCallGraphNode &) = delete;
+  SimplifiedCallGraphNode(const SimplifiedCallGraphNode &) = delete;
+  SimplifiedCallGraphNode &operator=(const SimplifiedCallGraphNode &) = delete;
 
-  ~SimplifyCallGraphNode() {}
+  ~SimplifiedCallGraphNode() = default;
 
   Function *getFunction() const { return F; }
 
   unsigned getNumReferences() const { return NumReferences; }
 
-  using iterator = DenseSet<SimplifyCallGraphNode *>::iterator;
-  using const_iterator = DenseSet<SimplifyCallGraphNode *>::const_iterator;
+  using iterator = DenseSet<SimplifiedCallGraphNode *>::iterator;
+  using const_iterator = DenseSet<SimplifiedCallGraphNode *>::const_iterator;
 
   inline iterator begin() { return CalledFunctions.begin(); }
   inline iterator end() { return CalledFunctions.end(); }
   inline const_iterator begin() const { return CalledFunctions.begin(); }
   inline const_iterator end() const { return CalledFunctions.end(); }
-  inline size_t count(SimplifyCallGraphNode * SCGNode) { return CalledFunctions.count(SCGNode); }
   inline bool empty() const { return CalledFunctions.empty(); }
   inline unsigned size() const { return (unsigned)CalledFunctions.size(); }
 
-  void addCalledFunction(SimplifyCallGraphNode *Called) {
+  void addCalledFunction(SimplifiedCallGraphNode *Called) {
     auto [It, Inserted] = CalledFunctions.insert(Called);
     if (Inserted)
-      Called->AddRef();
-  }
-
-  void removeCalledFunction(SimplifyCallGraphNode *Called) {
-    auto NumRemoved = CalledFunctions.erase(Called);
-    if (NumRemoved > 0)
-      Called->DropRef();
+      Called->addRef();
   }
 
 private:
-  friend class SimplifyCallGraph;
+  friend class SimplifiedCallGraph;
 
   Function *F;
 
-  DenseSet<SimplifyCallGraphNode *> CalledFunctions;
+  DenseSet<SimplifiedCallGraphNode *> CalledFunctions;
   unsigned NumReferences = 0;
 
-  void DropRef() { --NumReferences; }
-  void AddRef() { ++NumReferences; }
+  void addRef() { ++NumReferences; }
 };
 
-static void addAllDependencies(SimplifyCallGraph &SCG, const Function &F,
+/// Collect \p F and all non-declaration functions transitively called by \p F,
+/// using the SimplifiedCallGraph \p SCG, and insert them into \p Fns.
+static void addAllDependencies(SimplifiedCallGraph &SCG, const Function &F,
                                DenseSet<const Function *> &Fns) {
   assert(!F.isDeclaration());
   SmallVector<const Function *> WorkList({&F});
+  Fns.insert(&F);
 
   while (!WorkList.empty()) {
     const auto &CurFn = *WorkList.pop_back_val();
     assert(!CurFn.isDeclaration());
 
-    // Walk the callees of CurFn recorded in SimplifyCallGraph and
+    // Walk the callees of CurFn recorded in SimplifiedCallGraph and
     // add them to Fns, recursing transitively via the WorkList.
     for (auto &SCGNode : *SCG.at(&CurFn)) {
       auto *Callee = SCGNode->getFunction();
       if (!Callee || Callee->isDeclaration())
         continue;
-      // Don't recurse into the starting function itself (would re-add F).
-      if (Callee == &F)
-        continue;
       if (Fns.insert(Callee).second)
         WorkList.push_back(Callee);
     }
   }
 }
 
+/// The root function of the call graph, along with its transitive dependency
+/// closure and cumulative cost. Used by createWorkList to build the
+/// partitioning worklist and by doPartitioning for load-balanced
+/// bin-packing; it is the smallest unit allocated by doPartitioning.
 struct FunctionWithDependencies {
-  FunctionWithDependencies(SimplifyCallGraph &SCG,
+  FunctionWithDependencies(SimplifiedCallGraph &SCG,
                            const DenseMap<const Function *, CostType> &FnCosts,
                            const Function *F)
       : F(F) {
     addAllDependencies(SCG, *F, Dependencies);
 
-    TotalCost = FnCosts.at(F);
-    for (const auto *Dep : Dependencies) {
+    for (const auto *Dep : Dependencies)
       TotalCost += FnCosts.lookup(Dep);
-    }
   }
 
+  // The root function of the call graph.
   const Function *F = nullptr;
+  // Transitive closure of non-declaration functions called by F (includes F).
   DenseSet<const Function *> Dependencies;
+  // Sum of IR-instruction counts over F and all its dependencies.
   CostType TotalCost = 0;
 };
 
@@ -192,15 +172,14 @@ struct FunctionWithDependencies {
 ///   1. externalize(): promote local symbols to external+hidden so they are
 ///      visible across partitions. Unnamed entities get a stable name.
 ///   2. calculateFunctionCosts(): compute per-function IR instruction counts.
-///   3. createWorkList(): walk SimplifyCallGraph to discover call-graph roots
+///   3. createWorkList(): walk SimplifiedCallGraph to discover call-graph roots
 ///      and their transitive dependencies.
 ///   4. doPartitioning(): greedily assign each root + dependencies to the
 ///      least-loaded partition, balancing by accumulated cost.
 ///   5. For each partition: CloneModule the original module filtered by
-///      ShouldCloneDefinition, then dealWithMpart cleans up unused locals,
-///      marks available-externally-defined functions, and records (in
-///      PromotedRenames) the renaming for promoted locals so the caller can
-///      apply it later.
+///      ShouldCloneDefinition, then dealWithMpart downgrades duplicate
+///      external function definitions to available_externally and renames
+///      promoted locals to avoid duplicate symbols across partitions.
 ///   6. Each partition bitcode is serialized to its own LLVMContext (via
 ///      write+read) so partitions can be processed on concurrent threads
 ///      without sharing LLVMContext state.
@@ -217,25 +196,19 @@ class SplitModuleCG {
   ///          produce. Pass 0 (the default) to derive the partition count
   ///          from the number of call-graph roots discovered in
   ///          createWorkList (one root per partition at most). The actual
-  ///          partition count is finalized in the constructor and can be
-  ///          queried via getPartitionNum().
+  ///          partition count is finalized in the constructor.
   SplitModuleCG(Module &M, unsigned LimitPartition = 0);
   void SplitModule(ModuleCreationCallback ModuleCallback,
                    const llvm::lto::Config &C);
 
-  unsigned getPartitionNum() { return N; }
-  StringSet<> &getOriginalExternals() { return OriginalExternals; }
-  StringMap<std::string> &getPromotedRenames() { return PromotedRenames; }
-
 private:
   unsigned N;
   Module &M;
   CallGraph CG;
-  std::unique_ptr<SimplifyCallGraph> SCG;
+  std::unique_ptr<SimplifiedCallGraph> SCG;
   CostType ModuleCost;
   DenseSet<const Function *> EntryFuncs;
   StringSet<> OriginalExternals;
-  StringMap<std::string> PromotedRenames;
   DenseMap<const Function *, bool> externalFunction;
   DenseMap<const Function *, CostType> FuncsCosts;
   SmallVector<FunctionWithDependencies> FWDWorkList;
@@ -250,18 +223,11 @@ class SplitModuleCG {
   std::vector<DenseSet<const Function *>> doPartitioning();
 
   /// Post-process a cloned partition \p MPart (partition index \p I):
-  ///   - Record promoted names for symbols that were local but
-  ///     are now external (not in OriginalExternals) into PromotedRenames.
-  ///   - Erase conservatively-cloned local globals that ended up with no users.
-  ///   - For functions that were already external in the source module and
-  ///     are being defined in this partition, downgrade their duplicate
-  ///     definitions in other partitions to available_externally via the
-  ///     externalFunction map.
-  /// \p NeedsConservativeImport is the predicate (captured by SplitModule)
-  /// that identifies local globals that must be cloned into every partition.
-  void dealWithMpart(
-      Module &MPart, unsigned I,
-      function_ref<bool(const GlobalValue *)> NeedsConservativeImport);
+  ///   - Downgrade duplicate definitions of originally-external functions to
+  ///     available_externally.
+  ///   - Rename promoted local symbols (now external, not in OriginalExternals)
+  ///     to "name.llvm.<suffix>" to avoid duplicate symbols across partitions.
+  void dealWithMpart(Module &MPart, unsigned I);
 
   /// Discover call-graph roots (functions with in-degree 0 in SCG) and
   /// build FWDWorkList, where each entry is a root + its transitive
diff --git a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
index 16032a3336f1d..ca924215f1a14 100644
--- a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
+++ b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
@@ -11,13 +11,13 @@
 #include <thread>
 using namespace llvm;
 
-#define DEBUG_TYPE "split-module-CG"
+#define DEBUG_TYPE "split-module-cg"
 
 namespace {
 
-static cl::opt<bool> enablePrintSimplifyCallGraph(
-    "enable-print-simplify-callgraph", cl::Hidden, cl::init(false),
-    cl::desc("print SimplifyCallGraph"));
+static cl::opt<bool> enablePrintSimplifiedCallGraph(
+    "enable-print-simplified-callgraph", cl::Hidden, cl::init(false),
+    cl::desc("print SimplifiedCallGraph"));
 
 using PartitionID = unsigned;
 
@@ -38,10 +38,9 @@ static void externalize(GlobalValue *GV) {
 std::vector<DenseSet<const Function *>> SplitModuleCG::doPartitioning() {
   LLVM_DEBUG(dbgs() << "\n--Partitioning Starts--\n");
   // Performs all of the partitioning work on M.
+  assert(N != 0 && "Partition count must be at least 1");
   std::vector<DenseSet<const Function *>> Partitions;
   Partitions.resize(N);
-  if (N == 0)
-    return Partitions;
 
   auto ComparePartitions = [](const std::pair<PartitionID, CostType> &a,
                               const std::pair<PartitionID, CostType> &b) {
@@ -57,18 +56,16 @@ std::vector<DenseSet<const Function *>> SplitModuleCG::doPartitioning() {
   for (unsigned I = 0; I < N; ++I)
     BalancingQueue.emplace_back(I, 0);
 
-  // Helper function to handle assigning a function to a partition. This takes
-  // care of updating the balancing queue.
-  const auto AssignToPartition = [&](PartitionID PID,
-                                     const FunctionWithDependencies &FWD) {
+  for (auto &CurFn : FWDWorkList) {
+    // Normal "load-balancing", assign to partition with least pressure.
+    auto [PID, _] = BalancingQueue.back();
+
     // Insert the root function and its dependencies into the partition,
     // tracking the cost of newly inserted functions so the balancing queue
-    // can be updated.
+    // can be updated. CurFn.Dependencies includes the root F itself.
     auto &FnsInPart = Partitions[PID];
     CostType AddedCost = 0;
-    if (FnsInPart.insert(FWD.F).second)
-      AddedCost += FuncsCosts.at(FWD.F);
-    for (const Function *Dep : FWD.Dependencies)
+    for (const Function *Dep : CurFn.Dependencies)
       if (FnsInPart.insert(Dep).second)
         AddedCost += FuncsCosts.lookup(Dep);
 
@@ -81,12 +78,6 @@ std::vector<DenseSet<const Function *>> SplitModuleCG::doPartitioning() {
     }
 
     sort(BalancingQueue, ComparePartitions);
-  };
-
-  for (auto &CurFn : FWDWorkList) {
-    // Normal "load-balancing", assign to partition with least pressure.
-    auto [PID, CurCost] = BalancingQueue.back();
-    AssignToPartition(PID, CurFn);
   }
 
   return Partitions;
@@ -99,10 +90,8 @@ void SplitModuleCG::calculateFunctionCosts() {
       continue;
 
     CostType FnCost = 0;
-    for (const auto &BB : Fn) {
-      CostType CostVal = std::distance(BB.begin(), BB.end());
-      FnCost += CostVal;
-    }
+    for (const auto &BB : Fn)
+      FnCost += std::distance(BB.begin(), BB.end());
     assert(FnCost != 0);
     FuncsCosts[&Fn] = FnCost;
     assert((ModuleCost + FnCost) >= ModuleCost && "Overflow!");
@@ -110,71 +99,62 @@ void SplitModuleCG::calculateFunctionCosts() {
   }
 }
 
-void SplitModuleCG::dealWithMpart(Module &MPart, unsigned I,
-                                   function_ref<bool(const GlobalValue *)> NeedsConservativeImport) {
-  // Collect promoted symbols (those that were local but are now external due
-  // to externalize(), and therefore are not in the OriginalExternals set
-  // captured at construction time).
-  //
-  // Note: here we only *record* the rename in PromotedRenames; we do not
-  // perform the actual renaming immediately. The rename is applied after the
-  // opt pipeline has completed. This is intentional: deferring the rename
-  // minimizes the impact of renaming on subsequent optimizations.
-  auto checkPromoted = [&](const GlobalValue &GV) {
-    // now is external (not local), but not in external set.
-    if (!GV.hasLocalLinkage() && !OriginalExternals.contains(GV.getName())) {
-      if (PromotedRenames.count(GV.getName()))
-        return;
-      // Use the naming convention "name.llvm.<suffix>" so the
-      // promoted local cannot clash with an external that happens to share
-      // the same name in another module/partition.
-      std::string Suffix = getUniqueModuleId(&M);
-      std::string NewName = (GV.getName() + ".llvm" + Suffix).str();
-      PromotedRenames[GV.getName()] = NewName;
-    }
-  };
-
-  auto AvailableExternalizeFunc = [&](llvm::Function &Func) {
-    Func.setLinkage(GlobalValue::AvailableExternallyLinkage);
-    Func.setComdat(nullptr);
-  };
-
-  for (const auto &GV : MPart.global_values())
-    checkPromoted(GV);
-  // Clean-up conservatively imported GVs without any users.
-  for (auto &GV : make_early_inc_range(MPart.globals())) {
-    if (NeedsConservativeImport(&GV) && GV.use_empty())
-      GV.eraseFromParent();
-  }
-
+void SplitModuleCG::dealWithMpart(Module &MPart, unsigned I) {
+  // Downgrade duplicate definitions of external functions to
+  // available_externally. The first partition to define such a function keeps
+  // the real definition; all other partitions get available_externally copies.
   for (auto &func : MPart.functions()) {
+    if (func.isDeclaration())
+      continue;
     auto Fn = M.getFunction(func.getName());
-    if (externalFunction.count(Fn) && !func.isDeclaration()) {
-      if (!externalFunction[Fn]) {
-        AvailableExternalizeFunc(func);
-      } else {
-        externalFunction[Fn] = false;
-      }
+    if (!externalFunction.contains(Fn))
+      continue;
+    if (!externalFunction[Fn]) {
+      func.setLinkage(GlobalValue::AvailableExternallyLinkage);
+      func.setComdat(nullptr);
+    } else {
+      externalFunction[Fn] = false;
     }
   }
 
+  // Rename GlobalValues whose linkage was promoted from local to external,
+  // to avoid duplicate symbols across partitions in ThinLTO. Use the naming
+  // convention "name.llvm.<suffix>" so the promoted local cannot clash with
+  // an external that happens to share the same name. The suffix is derived
+  // from the module via getUniqueModuleId, so it is consistent across all
+  // partitions.
+  std::string Suffix = getUniqueModuleId(&M);
+  for (auto &GV : MPart.global_values()) {
+    // Now external (not local), but was not originally external.
+    if (GV.hasLocalLinkage() || OriginalExternals.contains(GV.getName()))
+      continue;
+    // Skip declarations of functions that were not explicitly externalized
+    // (e.g. skipped by the hasOneUse check). Their definitions in other
+    // partitions remain internal and are not renamed, so declarations must
+    // keep the original name to stay consistent.
+    auto *Fn = dyn_cast<Function>(&GV);
+    if (Fn && Fn->isDeclaration() &&
+        !externalFunction.contains(M.getFunction(Fn->getName())))
+      continue;
+    GV.setName((GV.getName() + ".llvm" + Suffix).str());
+  }
+
+#ifndef NDEBUG
   LLVM_DEBUG(dbgs() << MPart.getModuleIdentifier() << "  : \n");
-  for (auto &F : MPart) {
+  for (auto &F : MPart)
     if (!F.isDeclaration())
       LLVM_DEBUG(dbgs() << "   [Function: ] " << I << "  " << F.getName() << " "
                         << F.getLinkage() << "\n");
-  }
+#endif
 }
 
 void SplitModuleCG::createWorkList() {
   // First, find all the entry functions with an in-degree of 0
   // (i.e., those that are not called by any function).
-  for (auto &NodePair : *SCG) {
-    SimplifyCallGraphNode *SCGNode = NodePair.second.get();
+  for (auto &SCGNode : SCG->values()) {
     Function *F = SCGNode->getFunction();
-    if (F && SCGNode->getNumReferences() == 0) {
+    if (F && SCGNode->getNumReferences() == 0)
       EntryFuncs.insert(F);
-    }
   }
 
   // Second, find all the dependencies of each entry function.
@@ -185,19 +165,17 @@ void SplitModuleCG::createWorkList() {
   // Third, find all the functions that are not in the worklist.
   DenseSet<const Function *> SeenFunctions;
   for (const auto &FWD : FWDWorkList) {
-    SeenFunctions.insert(FWD.F);
     SeenFunctions.insert(FWD.Dependencies.begin(), FWD.Dependencies.end());
   }
   for (auto &F : M) {
-    // This function may be in a ring, and therefore is not a dependency of
+    // This function may be in a cycle, and therefore is not a dependency of
     // any root, which is treated as a root function here.
-    if (!F.isDeclaration() && !SeenFunctions.count(&F)) {
-      FWDWorkList.emplace_back(*SCG, FuncsCosts, &F);
-      auto &FWD = FWDWorkList.back();
-      EntryFuncs.insert(&F);
-      SeenFunctions.insert(FWD.F);
-      SeenFunctions.insert(FWD.Dependencies.begin(), FWD.Dependencies.end());
-    }
+    if (F.isDeclaration() || SeenFunctions.contains(&F))
+      continue;
+    FWDWorkList.emplace_back(*SCG, FuncsCosts, &F);
+    auto &FWD = FWDWorkList.back();
+    EntryFuncs.insert(&F);
+    SeenFunctions.insert(FWD.Dependencies.begin(), FWD.Dependencies.end());
   }
 
   // Sort the worklist so the most expensive roots are seen first.
@@ -213,12 +191,13 @@ void SplitModuleCG::createWorkList() {
                     << FWDWorkList.size() << "   Module cost: "
                     << ModuleCost << "\n");
   LLVM_DEBUG(dbgs() << "callgraphs: \n");
-  for (auto FWD : FWDWorkList) {
+#ifndef NDEBUG
+  for (auto FWD : FWDWorkList)
     LLVM_DEBUG(dbgs() << "[root] " << FWD.F->getName() << " (totalCost:"
                       << FWD.TotalCost << ";   root function cost: "
                       << FuncsCosts[FWD.F] << ";   has dependency: "
                       << FWD.Dependencies.size() << "\n");
-  }
+#endif
 }
 
 void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
@@ -227,6 +206,11 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
     if (F.hasLocalLinkage() && F.hasOneUse() && !F.hasAddressTaken())
       continue;
     externalize(&F);
+    // Record functions that may be defined in multiple partitions so that
+    // dealWithMpart can downgrade duplicates to available_externally. This
+    // includes functions with external linkage (either originally or just
+    // promoted by externalize), as well as functions whose definitions are
+    // not exact (e.g. linkonce/weak), which may be replaced at link time.
     if (!F.isDeclaration() &&
         (F.hasExternalLinkage() || !F.isDefinitionExact()))
       externalFunction[&F] = true;
@@ -242,23 +226,12 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
   auto Partitions = doPartitioning();
   assert(Partitions.size() == N);
 
-  const auto NeedsConservativeImport = [&](const GlobalValue *GV) {
-    // Conservatively clone private/internal globals into every partition;
-    // unused copies are removed by dealWithMpart afterwards.
-    const auto *Var = dyn_cast<GlobalVariable>(GV);
-    return Var && Var->hasLocalLinkage();
-  };
-
   auto ShouldCloneDefinition = [&](unsigned I, const GlobalValue *GV) {
     const auto &FnsInPart = Partitions[I];
 
     // Functions go in their assigned partition.
-    if (const auto *newFn = dyn_cast<Function>(GV)) {
-      const auto *Fn = M.getFunction(newFn->getName());
-      return FnsInPart.contains(Fn);
-    }
-    if (NeedsConservativeImport(GV))
-      return true;
+    if (const auto *FnToClone = dyn_cast<Function>(GV))
+      return FnsInPart.contains(FnToClone);
     // Everything else goes in the first partition.
     return I == 0;
   };
@@ -272,8 +245,6 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
   // straightforward.
   std::vector<std::thread> Threads;
   Threads.reserve(N);
-  std::vector<std::unique_ptr<Module>> MPartInCtxs;
-  MPartInCtxs.resize(N);
   for (unsigned I = 0; I < N; ++I) {
     ValueToValueMapTy VMap;
     std::unique_ptr<Module> MPart(
@@ -281,7 +252,7 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
         return ShouldCloneDefinition(I, GV);
     }));
 
-    dealWithMpart(*MPart, I, NeedsConservativeImport);
+    dealWithMpart(*MPart, I);
 
     // Serialize the cloned partition to bitcode and re-parse it inside the
     // worker thread's own LLVMContext. This round-trip is required because
@@ -320,27 +291,27 @@ SplitModuleCG::SplitModuleCG(Module &M, unsigned LimitPartition)
   calculateFunctionCosts();
 
   // Construct a simplified call graph to facilitate worklist generation.
-  SCG = std::make_unique<SimplifyCallGraph>(CG, M);
+  SCG = std::make_unique<SimplifiedCallGraph>(CG);
 
   // Populate the worklist with root functions and their transitive
   // dependencies. This worklist serves as the foundation for the
   // subsequent module partitioning.
   createWorkList();
 
-  if (N == 0 || N > EntryFuncs.size()) {
+  if (N == 0 || N > EntryFuncs.size())
     N = EntryFuncs.size();
-  }
-  N = N == 0 ? 1 : N;
+  if (N == 0)
+    N = 1;
 }
 
-void SimplifyCallGraph::createSimplifyCallGraph() {
+SimplifiedCallGraph::SimplifiedCallGraph(CallGraph &CG) {
   for (auto &NodePair : CG) {
-    CallGraphNode *CGNode = NodePair.second.get();
+    auto &CGNode = NodePair.second;
     Function *F = CGNode->getFunction();
     if (!F || F->isDeclaration())
       continue;
 
-    SimplifyCallGraphNode *SCGNode = getOrInsertFunction(F);
+    SimplifiedCallGraphNode *SCGNode = getOrInsertFunction(F);
 
     for (const auto &CGNodeItem : *CGNode) {
       Function *Called = CGNodeItem.second->getFunction();
@@ -350,31 +321,32 @@ void SimplifyCallGraph::createSimplifyCallGraph() {
     }
   }
 
-  if (enablePrintSimplifyCallGraph)
+  if (enablePrintSimplifiedCallGraph)
     print();
 }
 
 
-void SimplifyCallGraph::print() {
+void SimplifiedCallGraph::print() {
+#ifndef NDEBUG
   for (auto &SCGItem : FunctionMap) {
     LLVM_DEBUG(dbgs() << "Call graph node for function: '"
                       << SCGItem.first->getName() << "' #uses="
                       << SCGItem.second->getNumReferences() << "\n");
 
-    for (const auto &callee : *SCGItem.second) {
+    for (const auto &callee : *SCGItem.second)
       LLVM_DEBUG(dbgs() <<"          Calls function : '"
                         << callee->getFunction()->getName() << " '\n");
-    }
   }
+#endif
 }
 
-SimplifyCallGraphNode *
-SimplifyCallGraph::getOrInsertFunction(const Function *F) {
+SimplifiedCallGraphNode *
+SimplifiedCallGraph::getOrInsertFunction(const Function *F) {
   auto &SCGN = FunctionMap[F];
   if (SCGN)
     return SCGN.get();
 
   SCGN =
-      std::make_unique<SimplifyCallGraphNode>(this, const_cast<Function *>(F));
+      std::make_unique<SimplifiedCallGraphNode>(const_cast<Function *>(F));
   return SCGN.get();
 }
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/function-with-ring.ll b/llvm/test/tools/llvm-split/SplitModuleCG/function-with-cycle.ll
similarity index 75%
rename from llvm/test/tools/llvm-split/SplitModuleCG/function-with-ring.ll
rename to llvm/test/tools/llvm-split/SplitModuleCG/function-with-cycle.ll
index f2fc8c03c922a..0d31bd7ec5649 100644
--- a/llvm/test/tools/llvm-split/SplitModuleCG/function-with-ring.ll
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/function-with-cycle.ll
@@ -1,7 +1,10 @@
-; RUN: llvm-split -enable-split-module-CG=true -j2 -o %t %s
+; RUN: llvm-split -enable-call-graph-split-module=true -j2 -o %t %s
 ; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
 ; RUN: llvm-dis -o - %t1 | FileCheck --check-prefix=CHECK1 %s
 
+; Test splitting when the call graph contains a cycle (foo -> call_foo -> foo),
+; verifying that cycle members land in the same partition.
+
 ; CHECK0-DAG: declare void @foo()
 ; CHECK0-DAG: define void @bar()
 ; CHECK0-DAG: declare void @call_foo()
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/function.ll b/llvm/test/tools/llvm-split/SplitModuleCG/function.ll
index ddf5bb5c3dff3..86eb1f6c60d97 100644
--- a/llvm/test/tools/llvm-split/SplitModuleCG/function.ll
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/function.ll
@@ -1,7 +1,10 @@
-; RUN: llvm-split -enable-split-module-CG=true -j2 -o %t %s
+; RUN: llvm-split -enable-call-graph-split-module=true -j2 -o %t %s
 ; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
 ; RUN: llvm-dis -o - %t1 | FileCheck --check-prefix=CHECK1 %s
 
+; Test basic call graph based module splitting — functions are grouped into
+; partitions by their call relationships.
+
 ; CHECK0-DAG: declare dso_local void @foo()
 ; CHECK0-DAG: define void @bar()
 ; CHECK0-DAG: declare void @func_a()
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/partition-cap.ll b/llvm/test/tools/llvm-split/SplitModuleCG/partition-cap.ll
index 5c3ced3e682af..a0e7101ecad0c 100644
--- a/llvm/test/tools/llvm-split/SplitModuleCG/partition-cap.ll
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/partition-cap.ll
@@ -1,7 +1,9 @@
-; RUN: llvm-split -enable-split-module-CG=true -j10 -o %t %s
+; RUN: llvm-split -enable-call-graph-split-module=true -j10 -o %t %s
 ; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
 ; RUN: llvm-dis -o - %t1 | FileCheck --check-prefix=CHECK1 %s
-; should only produce 2 output files (N capped to EntryFuncs.size()=2)
+
+; Test that partition count is capped to the number of entry functions
+; (-j10 but only 2 roots → 2 outputs).
 
 ; CHECK0: define void @foo()
 ; CHECK1: define void @bar()
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/promoted-rename.ll b/llvm/test/tools/llvm-split/SplitModuleCG/promoted-rename.ll
new file mode 100644
index 0000000000000..3dd6ae8ed9527
--- /dev/null
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/promoted-rename.ll
@@ -0,0 +1,23 @@
+; Test that an internal function called by multiple roots is externalized
+; and renamed with a .llvm.<suffix> suffix in all partitions.
+
+; RUN: llvm-split -enable-call-graph-split-module=true -j2 -o %t %s
+; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
+; RUN: llvm-dis -o - %t1 | FileCheck --check-prefix=CHECK1 %s
+
+; CHECK0-DAG: define hidden void @helper.llvm.{{[0-9a-f]+}}()
+; CHECK1-DAG: define available_externally hidden  void @helper.llvm.{{[0-9a-f]+}}()
+
+define internal void @helper() {
+  ret void
+}
+
+define void @caller1() {
+  call void @helper()
+  ret void
+}
+
+define void @caller2() {
+  call void @helper()
+  ret void
+}
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/single-partition.ll b/llvm/test/tools/llvm-split/SplitModuleCG/single-partition.ll
index fdfdf910a3498..03b1454c0e2a8 100644
--- a/llvm/test/tools/llvm-split/SplitModuleCG/single-partition.ll
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/single-partition.ll
@@ -1,6 +1,8 @@
-; RUN: llvm-split -enable-split-module-CG=true -j1 -o %t %s
+; RUN: llvm-split -enable-call-graph-split-module=true -j1 -o %t %s
 ; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
 
+; Test that -j1 places all functions into a single partition.
+
 ; CHECK0: define void @foo()
 ; CHECK0: define void @bar()
 
@@ -8,6 +10,7 @@ define void @foo() {
   call void @bar()
   ret void
 }
+
 define void @bar() {
   ret void
 }
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll b/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll
index 73f7079669c55..228294c2ef475 100644
--- a/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll
@@ -1,8 +1,11 @@
-; RUN: llvm-split -enable-split-module-CG=true -j2 -o %t %s
+; RUN: llvm-split -enable-call-graph-split-module=true -j2 -o %t %s
 ; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
 
-; CHECK0-DAG: define hidden void @__llvmsplit_unnamed()
+; Test that an unnamed internal function (@0) is given a stable name and
+; .llvm.<suffix> suffix after promotion.
+
+; CHECK0-DAG: {{define hidden void @__llvmsplit_unnamed\.llvm\.}}
 
 define internal void @0() {
   ret void
-}
\ No newline at end of file
+}
diff --git a/llvm/tools/llvm-split/llvm-split.cpp b/llvm/tools/llvm-split/llvm-split.cpp
index 69c35e5f0d8af..194248b1ea603 100644
--- a/llvm/tools/llvm-split/llvm-split.cpp
+++ b/llvm/tools/llvm-split/llvm-split.cpp
@@ -80,8 +80,10 @@ static cl::opt<std::string>
          cl::value_desc("cpu"), cl::cat(SplitCategory));
          
 static cl::opt<bool>
-    EnableSplitModuleCG("enable-split-module-CG", cl::Prefix, cl::init(false),
-     cl::desc("Split module using call graph"), cl::cat(SplitCategory));
+    EnableCallGraphSplitModule("enable-call-graph-split-module",
+                               cl::Prefix, cl::init(false),
+                               cl::desc("Split module using call graph"),
+                               cl::cat(SplitCategory));
 
 enum class SplitByCategoryType {
   SBCT_ByAttribute,
@@ -330,7 +332,7 @@ int main(int argc, char **argv) {
               "splitModule implementation\n";
   }
 
-  if (EnableSplitModuleCG) {
+  if (EnableCallGraphSplitModule) {
     const auto HandleModulePartCG = [&](std::unique_ptr<Module> MPart, unsigned I) {
       std::error_code EC;
       std::unique_ptr<ToolOutputFile> Out(

>From 64a7301983d4da79a0aef2425a1ac706c89c418b Mon Sep 17 00:00:00 2001
From: maojiaping <maojiaping1 at huawei.com>
Date: Fri, 4 Sep 2026 15:23:34 +0800
Subject: [PATCH 10/13] [SplitModuleCG] Address review feedback: naming,
 linkage handling, and formatting

- Rename local variables to UpperCamelCase per LLVM conventions.
- Only downgrade weak_odr/linkonce_odr (not interposable) functions to
  available_externally; extract into canDowngradeToAvailableExternally().
- Clarify comments and add odr-linkage.ll test.
- Run clang-format.
---
 .../llvm/Transforms/Utils/SplitModuleCG.h     |   7 +-
 llvm/lib/Transforms/Utils/SplitModuleCG.cpp   | 119 ++++++++++--------
 .../llvm-split/SplitModuleCG/odr-linkage.ll   |  46 +++++++
 llvm/tools/llvm-split/llvm-split.cpp          |  14 +--
 4 files changed, 122 insertions(+), 64 deletions(-)
 create mode 100644 llvm/test/tools/llvm-split/SplitModuleCG/odr-linkage.ll

diff --git a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
index 4f45fb82fe501..f07326773fcc9 100644
--- a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
+++ b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
@@ -1,11 +1,11 @@
 #ifndef LLVM_TRANSFORMS_UTILS_SPLITMODULECG_H
 #define LLVM_TRANSFORMS_UTILS_SPLITMODULECG_H
 
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/DenseSet.h"
 #include "llvm/ADT/StringSet.h"
 #include "llvm/Analysis/CallGraph.h"
 #include "llvm/LTO/Config.h"
-#include "llvm/ADT/DenseMap.h"
-#include "llvm/ADT/DenseSet.h"
 #include <map>
 
 namespace llvm {
@@ -76,8 +76,7 @@ class SimplifiedCallGraph {
 /// can identify roots (in-degree 0) during partitioning.
 class SimplifiedCallGraphNode {
 public:
-  inline SimplifiedCallGraphNode(Function *F)
-      : F(F) {}
+  inline SimplifiedCallGraphNode(Function *F) : F(F) {}
 
   SimplifiedCallGraphNode(const SimplifiedCallGraphNode &) = delete;
   SimplifiedCallGraphNode &operator=(const SimplifiedCallGraphNode &) = delete;
diff --git a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
index ca924215f1a14..c5e573ef9f142 100644
--- a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
+++ b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
@@ -15,9 +15,10 @@ using namespace llvm;
 
 namespace {
 
-static cl::opt<bool> enablePrintSimplifiedCallGraph(
-    "enable-print-simplified-callgraph", cl::Hidden, cl::init(false),
-    cl::desc("print SimplifiedCallGraph"));
+static cl::opt<bool>
+    enablePrintSimplifiedCallGraph("enable-print-simplified-callgraph",
+                                   cl::Hidden, cl::init(false),
+                                   cl::desc("print SimplifiedCallGraph"));
 
 using PartitionID = unsigned;
 
@@ -33,6 +34,19 @@ static void externalize(GlobalValue *GV) {
     GV->setName("__llvmsplit_unnamed");
 }
 
+/// Returns whether duplicate definitions of \p F across partitions may be
+/// downgraded to available_externally. This is safe for external functions
+/// (either originally external or promoted by externalize), and for
+/// weak_odr/linkonce_odr functions whose equivalent definitions can be
+/// deduplicated to reduce codegen. Interposable linkages (weak/linkonce
+/// non-ODR) are excluded since downgrading them would change their
+/// optimization semantics.
+static bool canDowngradeToAvailableExternally(const Function &F) {
+  return !F.isDeclaration() &&
+         (F.hasExternalLinkage() || F.hasWeakODRLinkage() ||
+          F.hasLinkOnceODRLinkage());
+}
+
 } // namespace
 
 std::vector<DenseSet<const Function *>> SplitModuleCG::doPartitioning() {
@@ -42,14 +56,14 @@ std::vector<DenseSet<const Function *>> SplitModuleCG::doPartitioning() {
   std::vector<DenseSet<const Function *>> Partitions;
   Partitions.resize(N);
 
-  auto ComparePartitions = [](const std::pair<PartitionID, CostType> &a,
-                              const std::pair<PartitionID, CostType> &b) {
+  auto ComparePartitions = [](const std::pair<PartitionID, CostType> &LHS,
+                              const std::pair<PartitionID, CostType> &RHS) {
     // When two partitions have the same cost, assign to the one with the
     // biggest ID first. This allows us to put things in P0 last, because P0 may
     // have other stuff added later.
-    if (a.second == b.second)
-      return a.first < b.first;
-    return a.second > b.second;
+    if (LHS.second == RHS.second)
+      return LHS.first < RHS.first;
+    return LHS.second > RHS.second;
   };
 
   std::vector<std::pair<PartitionID, CostType>> BalancingQueue;
@@ -103,17 +117,19 @@ void SplitModuleCG::dealWithMpart(Module &MPart, unsigned I) {
   // Downgrade duplicate definitions of external functions to
   // available_externally. The first partition to define such a function keeps
   // the real definition; all other partitions get available_externally copies.
-  for (auto &func : MPart.functions()) {
-    if (func.isDeclaration())
+  for (auto &PartFunc : MPart.functions()) {
+    if (PartFunc.isDeclaration())
       continue;
-    auto Fn = M.getFunction(func.getName());
-    if (!externalFunction.contains(Fn))
+    // Look up the corresponding function in the original module M to check
+    // its externalFunction status.
+    auto *OrigFn = M.getFunction(PartFunc.getName());
+    if (!externalFunction.contains(OrigFn))
       continue;
-    if (!externalFunction[Fn]) {
-      func.setLinkage(GlobalValue::AvailableExternallyLinkage);
-      func.setComdat(nullptr);
+    if (!externalFunction[OrigFn]) {
+      PartFunc.setLinkage(GlobalValue::AvailableExternallyLinkage);
+      PartFunc.setComdat(nullptr);
     } else {
-      externalFunction[Fn] = false;
+      externalFunction[OrigFn] = false;
     }
   }
 
@@ -125,7 +141,9 @@ void SplitModuleCG::dealWithMpart(Module &MPart, unsigned I) {
   // partitions.
   std::string Suffix = getUniqueModuleId(&M);
   for (auto &GV : MPart.global_values()) {
-    // Now external (not local), but was not originally external.
+    // Only rename symbols that were promoted from local to external: skip
+    // those that are still local, and those that were already external in
+    // the source module (recorded in OriginalExternals).
     if (GV.hasLocalLinkage() || OriginalExternals.contains(GV.getName()))
       continue;
     // Skip declarations of functions that were not explicitly externalized
@@ -164,8 +182,8 @@ void SplitModuleCG::createWorkList() {
 
   // Third, find all the functions that are not in the worklist.
   DenseSet<const Function *> SeenFunctions;
-  for (const auto &FWD : FWDWorkList) {
-    SeenFunctions.insert(FWD.Dependencies.begin(), FWD.Dependencies.end());
+  for (const auto &Fwd : FWDWorkList) {
+    SeenFunctions.insert(Fwd.Dependencies.begin(), Fwd.Dependencies.end());
   }
   for (auto &F : M) {
     // This function may be in a cycle, and therefore is not a dependency of
@@ -173,9 +191,9 @@ void SplitModuleCG::createWorkList() {
     if (F.isDeclaration() || SeenFunctions.contains(&F))
       continue;
     FWDWorkList.emplace_back(*SCG, FuncsCosts, &F);
-    auto &FWD = FWDWorkList.back();
+    auto &Fwd = FWDWorkList.back();
     EntryFuncs.insert(&F);
-    SeenFunctions.insert(FWD.Dependencies.begin(), FWD.Dependencies.end());
+    SeenFunctions.insert(Fwd.Dependencies.begin(), Fwd.Dependencies.end());
   }
 
   // Sort the worklist so the most expensive roots are seen first.
@@ -188,15 +206,16 @@ void SplitModuleCG::createWorkList() {
   });
 
   LLVM_DEBUG(dbgs() << "Number of callgraphs to be allocated: "
-                    << FWDWorkList.size() << "   Module cost: "
-                    << ModuleCost << "\n");
+                    << FWDWorkList.size() << "   Module cost: " << ModuleCost
+                    << "\n");
   LLVM_DEBUG(dbgs() << "callgraphs: \n");
 #ifndef NDEBUG
-  for (auto FWD : FWDWorkList)
-    LLVM_DEBUG(dbgs() << "[root] " << FWD.F->getName() << " (totalCost:"
-                      << FWD.TotalCost << ";   root function cost: "
-                      << FuncsCosts[FWD.F] << ";   has dependency: "
-                      << FWD.Dependencies.size() << "\n");
+  for (auto Fwd : FWDWorkList)
+    LLVM_DEBUG(dbgs() << "[root] " << Fwd.F->getName()
+                      << " (totalCost:" << Fwd.TotalCost
+                      << ";   root function cost: " << FuncsCosts[Fwd.F]
+                      << ";   has dependency: " << Fwd.Dependencies.size()
+                      << "\n");
 #endif
 }
 
@@ -207,12 +226,8 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
       continue;
     externalize(&F);
     // Record functions that may be defined in multiple partitions so that
-    // dealWithMpart can downgrade duplicates to available_externally. This
-    // includes functions with external linkage (either originally or just
-    // promoted by externalize), as well as functions whose definitions are
-    // not exact (e.g. linkonce/weak), which may be replaced at link time.
-    if (!F.isDeclaration() &&
-        (F.hasExternalLinkage() || !F.isDefinitionExact()))
+    // dealWithMpart can downgrade duplicates to available_externally.
+    if (canDowngradeToAvailableExternally(F))
       externalFunction[&F] = true;
   }
   for (GlobalVariable &GV : M.globals())
@@ -248,9 +263,9 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
   for (unsigned I = 0; I < N; ++I) {
     ValueToValueMapTy VMap;
     std::unique_ptr<Module> MPart(
-      CloneModule(M, VMap, [&](const GlobalValue *GV) {
-        return ShouldCloneDefinition(I, GV);
-    }));
+        CloneModule(M, VMap, [&](const GlobalValue *GV) {
+          return ShouldCloneDefinition(I, GV);
+        }));
 
     dealWithMpart(*MPart, I);
 
@@ -265,15 +280,17 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
     raw_svector_ostream BCOS(BC);
     WriteBitcodeToFile(*MPart, BCOS);
     MPart.reset();
-    Threads.emplace_back([&, I](SmallString<0> BC) {
-      llvm::lto::LTOLLVMContext Ctx(C);
-      Expected<std::unique_ptr<Module>> MOrErr = parseBitcodeFile(
-          MemoryBufferRef(BC.str(), "ld-temp.o"), Ctx);
-      BC = SmallString<0>();
-      if (!MOrErr)
-        report_fatal_error("Failed to read bitcode");
-      ModuleCallback(std::move(MOrErr.get()), I);
-    }, std::move(BC));
+    Threads.emplace_back(
+        [&, I](SmallString<0> BC) {
+          llvm::lto::LTOLLVMContext Ctx(C);
+          Expected<std::unique_ptr<Module>> MOrErr =
+              parseBitcodeFile(MemoryBufferRef(BC.str(), "ld-temp.o"), Ctx);
+          BC = SmallString<0>();
+          if (!MOrErr)
+            report_fatal_error("Failed to read bitcode");
+          ModuleCallback(std::move(MOrErr.get()), I);
+        },
+        std::move(BC));
   }
   for (auto &T : Threads)
     T.join();
@@ -325,7 +342,6 @@ SimplifiedCallGraph::SimplifiedCallGraph(CallGraph &CG) {
     print();
 }
 
-
 void SimplifiedCallGraph::print() {
 #ifndef NDEBUG
   for (auto &SCGItem : FunctionMap) {
@@ -333,9 +349,9 @@ void SimplifiedCallGraph::print() {
                       << SCGItem.first->getName() << "' #uses="
                       << SCGItem.second->getNumReferences() << "\n");
 
-    for (const auto &callee : *SCGItem.second)
-      LLVM_DEBUG(dbgs() <<"          Calls function : '"
-                        << callee->getFunction()->getName() << " '\n");
+    for (const auto &Callee : *SCGItem.second)
+      LLVM_DEBUG(dbgs() << "          Calls function : '"
+                        << Callee->getFunction()->getName() << " '\n");
   }
 #endif
 }
@@ -346,7 +362,6 @@ SimplifiedCallGraph::getOrInsertFunction(const Function *F) {
   if (SCGN)
     return SCGN.get();
 
-  SCGN =
-      std::make_unique<SimplifiedCallGraphNode>(const_cast<Function *>(F));
+  SCGN = std::make_unique<SimplifiedCallGraphNode>(const_cast<Function *>(F));
   return SCGN.get();
 }
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/odr-linkage.ll b/llvm/test/tools/llvm-split/SplitModuleCG/odr-linkage.ll
new file mode 100644
index 0000000000000..bdd1ec641977f
--- /dev/null
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/odr-linkage.ll
@@ -0,0 +1,46 @@
+; Test handling of weak_odr and linkonce_odr functions across partitions:
+; ODR definitions are safe to downgrade to available_externally in duplicate
+; partitions, while interposable linkages (weak/linkonce non-ODR) must keep
+; real definitions everywhere.
+
+; RUN: llvm-split -enable-call-graph-split-module=true -j2 -o %t %s
+; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
+; RUN: llvm-dis -o - %t1 | FileCheck --check-prefix=CHECK1 %s
+
+; ODR function defined in both partitions: the first partition keeps the real
+; definition, the second gets an available_externally copy.
+; CHECK0-DAG: define weak_odr void @odr_func()
+; CHECK1-DAG: define available_externally void @odr_func()
+
+; Interposable functions must NOT be downgraded: every partition keeps a real
+; definition since the linker may pick any of them.
+; CHECK0-DAG: define weak void @weak_func()
+; CHECK1-DAG: define weak void @weak_func()
+; CHECK0-DAG: define linkonce void @linkonce_func()
+; CHECK1-DAG: define linkonce void @linkonce_func()
+
+define weak_odr void @odr_func() {
+  ret void
+}
+
+define weak void @weak_func() {
+  ret void
+}
+
+define linkonce void @linkonce_func() {
+  ret void
+}
+
+define void @caller1() {
+  call void @odr_func()
+  call void @linkonce_func()
+  call void @weak_func()
+  ret void
+}
+
+define void @caller2() {
+  call void @odr_func()
+  call void @linkonce_func()
+  call void @weak_func()
+  ret void
+}
diff --git a/llvm/tools/llvm-split/llvm-split.cpp b/llvm/tools/llvm-split/llvm-split.cpp
index 194248b1ea603..429f86ddbd6a7 100644
--- a/llvm/tools/llvm-split/llvm-split.cpp
+++ b/llvm/tools/llvm-split/llvm-split.cpp
@@ -78,12 +78,10 @@ static cl::opt<std::string>
 static cl::opt<std::string>
     MCPU("mcpu", cl::desc("Target CPU, ignored if --mtriple is not used"),
          cl::value_desc("cpu"), cl::cat(SplitCategory));
-         
-static cl::opt<bool>
-    EnableCallGraphSplitModule("enable-call-graph-split-module",
-                               cl::Prefix, cl::init(false),
-                               cl::desc("Split module using call graph"),
-                               cl::cat(SplitCategory));
+
+static cl::opt<bool> EnableCallGraphSplitModule(
+    "enable-call-graph-split-module", cl::Prefix, cl::init(false),
+    cl::desc("Split module using call graph"), cl::cat(SplitCategory));
 
 enum class SplitByCategoryType {
   SBCT_ByAttribute,
@@ -333,7 +331,8 @@ int main(int argc, char **argv) {
   }
 
   if (EnableCallGraphSplitModule) {
-    const auto HandleModulePartCG = [&](std::unique_ptr<Module> MPart, unsigned I) {
+    const auto HandleModulePartCG = [&](std::unique_ptr<Module> MPart,
+                                        unsigned I) {
       std::error_code EC;
       std::unique_ptr<ToolOutputFile> Out(
           new ToolOutputFile(OutputFilename + utostr(I), EC, sys::fs::OF_None));
@@ -362,4 +361,3 @@ int main(int argc, char **argv) {
   SplitModule(*M, NumOutputs, HandleModulePart, PreserveLocals, RoundRobin);
   return 0;
 }
-

>From f75cd9593d2a22ade29133770c6ff76a5f7728d3 Mon Sep 17 00:00:00 2001
From: maojiaping <maojiaping1 at huawei.com>
Date: Mon, 14 Sep 2026 10:47:39 +0800
Subject: [PATCH 11/13] [SplitModuleCG] Use shared externalize and extract
 sortWorkList

Replace the local externalize() helper with the shared
GlobalValue::externalize() and extract the worklist sorting from
createWorkList() into sortWorkList(), called after externalization so
unnamed local functions get their stable names before the (cost, name)
sort, keeping partitioning deterministic.
---
 .../llvm/Transforms/Utils/SplitModuleCG.h     |  5 ++-
 llvm/lib/Transforms/Utils/SplitModuleCG.cpp   | 43 ++++++++-----------
 .../tools/llvm-split/SplitModuleCG/unnamed.ll | 10 ++++-
 3 files changed, 31 insertions(+), 27 deletions(-)

diff --git a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
index f07326773fcc9..9ce82b97fdabe 100644
--- a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
+++ b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
@@ -208,7 +208,7 @@ class SplitModuleCG {
   CostType ModuleCost;
   DenseSet<const Function *> EntryFuncs;
   StringSet<> OriginalExternals;
-  DenseMap<const Function *, bool> externalFunction;
+  DenseMap<const Function *, bool> ExternalFunction;
   DenseMap<const Function *, CostType> FuncsCosts;
   SmallVector<FunctionWithDependencies> FWDWorkList;
 
@@ -235,6 +235,9 @@ class SplitModuleCG {
   /// is sorted by (TotalCost desc, Name asc) so the most expensive roots
   /// are assigned first during partitioning.
   void createWorkList();
+
+  /// Sorts FWDWorkList by total cost (descending) and then by function name.
+  void sortWorkList();
 };
 
 } // end namespace llvm
diff --git a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
index c5e573ef9f142..d204ab7523bbc 100644
--- a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
+++ b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
@@ -22,18 +22,6 @@ static cl::opt<bool>
 
 using PartitionID = unsigned;
 
-static void externalize(GlobalValue *GV) {
-  if (GV->hasLocalLinkage()) {
-    GV->setLinkage(GlobalValue::ExternalLinkage);
-    GV->setVisibility(GlobalValue::HiddenVisibility);
-  }
-
-  // Unnamed entities must be named consistently between modules. setName will
-  // give a distinct name to each such entity.
-  if (!GV->hasName())
-    GV->setName("__llvmsplit_unnamed");
-}
-
 /// Returns whether duplicate definitions of \p F across partitions may be
 /// downgraded to available_externally. This is safe for external functions
 /// (either originally external or promoted by externalize), and for
@@ -121,15 +109,15 @@ void SplitModuleCG::dealWithMpart(Module &MPart, unsigned I) {
     if (PartFunc.isDeclaration())
       continue;
     // Look up the corresponding function in the original module M to check
-    // its externalFunction status.
+    // its ExternalFunction status.
     auto *OrigFn = M.getFunction(PartFunc.getName());
-    if (!externalFunction.contains(OrigFn))
+    if (!ExternalFunction.contains(OrigFn))
       continue;
-    if (!externalFunction[OrigFn]) {
+    if (!ExternalFunction[OrigFn]) {
       PartFunc.setLinkage(GlobalValue::AvailableExternallyLinkage);
       PartFunc.setComdat(nullptr);
     } else {
-      externalFunction[OrigFn] = false;
+      ExternalFunction[OrigFn] = false;
     }
   }
 
@@ -152,7 +140,7 @@ void SplitModuleCG::dealWithMpart(Module &MPart, unsigned I) {
     // keep the original name to stay consistent.
     auto *Fn = dyn_cast<Function>(&GV);
     if (Fn && Fn->isDeclaration() &&
-        !externalFunction.contains(M.getFunction(Fn->getName())))
+        !ExternalFunction.contains(M.getFunction(Fn->getName())))
       continue;
     GV.setName((GV.getName() + ".llvm" + Suffix).str());
   }
@@ -195,9 +183,11 @@ void SplitModuleCG::createWorkList() {
     EntryFuncs.insert(&F);
     SeenFunctions.insert(Fwd.Dependencies.begin(), Fwd.Dependencies.end());
   }
+}
 
+void SplitModuleCG::sortWorkList() {
   // Sort the worklist so the most expensive roots are seen first.
-  sort(FWDWorkList, [&](auto &A, auto &B) {
+  sort(FWDWorkList, [](const auto &A, const auto &B) {
     // Sort by total cost, and if the total cost is identical, sort
     // alphabetically
     if (A.TotalCost == B.TotalCost)
@@ -210,12 +200,12 @@ void SplitModuleCG::createWorkList() {
                     << "\n");
   LLVM_DEBUG(dbgs() << "callgraphs: \n");
 #ifndef NDEBUG
-  for (auto Fwd : FWDWorkList)
+  for (auto &Fwd : FWDWorkList)
     LLVM_DEBUG(dbgs() << "[root] " << Fwd.F->getName()
                       << " (totalCost:" << Fwd.TotalCost
                       << ";   root function cost: " << FuncsCosts[Fwd.F]
                       << ";   has dependency: " << Fwd.Dependencies.size()
-                      << "\n");
+                      << ")\n");
 #endif
 }
 
@@ -224,18 +214,21 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
   for (Function &F : M) {
     if (F.hasLocalLinkage() && F.hasOneUse() && !F.hasAddressTaken())
       continue;
-    externalize(&F);
+    F.externalize();
     // Record functions that may be defined in multiple partitions so that
     // dealWithMpart can downgrade duplicates to available_externally.
     if (canDowngradeToAvailableExternally(F))
-      externalFunction[&F] = true;
+      ExternalFunction[&F] = true;
   }
   for (GlobalVariable &GV : M.globals())
-    externalize(&GV);
+    GV.externalize();
   for (GlobalAlias &GA : M.aliases())
-    externalize(&GA);
+    GA.externalize();
   for (GlobalIFunc &GI : M.ifuncs())
-    externalize(&GI);
+    GI.externalize();
+
+  // Sort the worklist here, after all potential renaming has occurred.
+  sortWorkList();
 
   // Assign callgraphs into N partitions.
   auto Partitions = doPartitioning();
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll b/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll
index 228294c2ef475..6fb33d39e9093 100644
--- a/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll
@@ -4,8 +4,16 @@
 ; Test that an unnamed internal function (@0) is given a stable name and
 ; .llvm.<suffix> suffix after promotion.
 
-; CHECK0-DAG: {{define hidden void @__llvmsplit_unnamed\.llvm\.}}
+; CHECK0-DAG: declare hidden void @__llvm_unnamed.llvm.{{[0-9a-f]+}}()
+; CHECK0-DAG: define hidden void @__llvm_unnamed.1.llvm.{{[0-9a-f]+}}()
+
+; CHECK1-DAG: define hidden void @__llvm_unnamed.llvm.{{[0-9a-f]+}}()
+; CHECK1-DAG: declare hidden void @__llvm_unnamed.1.llvm.{{[0-9a-f]+}}()
 
 define internal void @0() {
   ret void
 }
+
+define internal void @1() {
+  ret void
+}

>From 1ba10fe5e3158dcdbc025f51a037f09afb8a09c4 Mon Sep 17 00:00:00 2001
From: mmjjpp <maojiaping1 at huawei.com>
Date: Mon, 28 Sep 2026 12:09:24 +0000
Subject: [PATCH 12/13] [SplitModuleCG] Address review feedback: formatting,
 threading, and layering

- Follow the coding standards: lowerCamelCase splitModule(), UpperCamelCase
  NumPartitions, class-scoped CostType, no redundant inline or anonymous
  namespace, and file headers.
- Take a context factory instead of lto::Config, and replace the raw
  std::thread vector with DefaultThreadPool, capping the auto-derived
  partition count to the hardware parallelism.
- Erase unused declarations, use a fallback rename suffix, check the cost
  accumulation in unsigned arithmetic, and simplify the balancing queue
  update.
- Update tests and documentation accordingly.
---
 .../llvm/Transforms/Utils/SplitModuleCG.h     | 158 +++++++-------
 llvm/lib/Transforms/Utils/SplitModuleCG.cpp   | 202 +++++++++++++-----
 .../SplitModuleCG/alias-unsupported.ll        |  28 +++
 .../SplitModuleCG/function-with-cycle.ll      |   4 -
 .../llvm-split/SplitModuleCG/function.ll      |   3 -
 .../SplitModuleCG/ifunc-unsupported.ll        |  27 +++
 .../llvm-split/SplitModuleCG/module-asm.ll    |  24 +++
 .../llvm-split/SplitModuleCG/partition-cap.ll |   3 +-
 .../tools/llvm-split/SplitModuleCG/unnamed.ll |   3 +-
 llvm/tools/llvm-split/llvm-split.cpp          |   6 +-
 10 files changed, 316 insertions(+), 142 deletions(-)
 create mode 100644 llvm/test/tools/llvm-split/SplitModuleCG/alias-unsupported.ll
 create mode 100644 llvm/test/tools/llvm-split/SplitModuleCG/ifunc-unsupported.ll
 create mode 100644 llvm/test/tools/llvm-split/SplitModuleCG/module-asm.ll

diff --git a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
index 9ce82b97fdabe..2434fa5379909 100644
--- a/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
+++ b/llvm/include/llvm/Transforms/Utils/SplitModuleCG.h
@@ -1,3 +1,17 @@
+//===- SplitModuleCG.h - Split a module by its call graph -------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file defines the llvm::SplitModuleCG class, which splits a module
+// into partitions based on its call graph, so that the partitions can be
+// optimized and codegen'd in parallel.
+//
+//===----------------------------------------------------------------------===//
+
 #ifndef LLVM_TRANSFORMS_UTILS_SPLITMODULECG_H
 #define LLVM_TRANSFORMS_UTILS_SPLITMODULECG_H
 
@@ -5,16 +19,17 @@
 #include "llvm/ADT/DenseSet.h"
 #include "llvm/ADT/StringSet.h"
 #include "llvm/Analysis/CallGraph.h"
-#include "llvm/LTO/Config.h"
+#include "llvm/Support/InstructionCost.h"
+
 #include <map>
+#include <memory>
 
 namespace llvm {
 
+class LLVMContext;
 class SimplifiedCallGraph;
 class SimplifiedCallGraphNode;
 
-using CostType = InstructionCost::CostType;
-
 /// A simplified view of the LLVM CallGraph used by SplitModuleCG to drive
 /// callgraph-based module partitioning.
 ///
@@ -46,23 +61,23 @@ class SimplifiedCallGraph {
 
   /// Iterates over all (Function*, SimplifiedCallGraphNode) pairs in the
   /// call graph.
-  inline iterator begin() { return FunctionMap.begin(); }
-  inline iterator end() { return FunctionMap.end(); }
-  inline const_iterator begin() const { return FunctionMap.begin(); }
-  inline const_iterator end() const { return FunctionMap.end(); }
+  iterator begin() { return FunctionMap.begin(); }
+  iterator end() { return FunctionMap.end(); }
+  const_iterator begin() const { return FunctionMap.begin(); }
+  const_iterator end() const { return FunctionMap.end(); }
 
   /// Iterates over all SimplifiedCallGraphNode (unique_ptr) values.
   auto values() { return llvm::make_second_range(FunctionMap); }
   auto values() const { return llvm::make_second_range(FunctionMap); }
 
   /// Returns the call graph node for the provided function.
-  inline const SimplifiedCallGraphNode *at(const Function *F) const {
+  const SimplifiedCallGraphNode *at(const Function *F) const {
     const_iterator I = FunctionMap.find(F);
     assert(I != FunctionMap.end() && "Function not in callgraph!");
     return I->second.get();
   }
 
-  inline SimplifiedCallGraphNode *at(const Function *F) {
+  SimplifiedCallGraphNode *at(const Function *F) {
     return const_cast<SimplifiedCallGraphNode *>(
         static_cast<const SimplifiedCallGraph &>(*this).at(F));
   }
@@ -76,7 +91,7 @@ class SimplifiedCallGraph {
 /// can identify roots (in-degree 0) during partitioning.
 class SimplifiedCallGraphNode {
 public:
-  inline SimplifiedCallGraphNode(Function *F) : F(F) {}
+  SimplifiedCallGraphNode(Function *F) : F(F) {}
 
   SimplifiedCallGraphNode(const SimplifiedCallGraphNode &) = delete;
   SimplifiedCallGraphNode &operator=(const SimplifiedCallGraphNode &) = delete;
@@ -90,12 +105,14 @@ class SimplifiedCallGraphNode {
   using iterator = DenseSet<SimplifiedCallGraphNode *>::iterator;
   using const_iterator = DenseSet<SimplifiedCallGraphNode *>::const_iterator;
 
-  inline iterator begin() { return CalledFunctions.begin(); }
-  inline iterator end() { return CalledFunctions.end(); }
-  inline const_iterator begin() const { return CalledFunctions.begin(); }
-  inline const_iterator end() const { return CalledFunctions.end(); }
-  inline bool empty() const { return CalledFunctions.empty(); }
-  inline unsigned size() const { return (unsigned)CalledFunctions.size(); }
+  iterator begin() { return CalledFunctions.begin(); }
+  iterator end() { return CalledFunctions.end(); }
+  const_iterator begin() const { return CalledFunctions.begin(); }
+  const_iterator end() const { return CalledFunctions.end(); }
+  bool empty() const { return CalledFunctions.empty(); }
+  unsigned size() const {
+    return static_cast<unsigned>(CalledFunctions.size());
+  }
 
   void addCalledFunction(SimplifiedCallGraphNode *Called) {
     auto [It, Inserted] = CalledFunctions.insert(Called);
@@ -114,44 +131,19 @@ class SimplifiedCallGraphNode {
   void addRef() { ++NumReferences; }
 };
 
-/// Collect \p F and all non-declaration functions transitively called by \p F,
-/// using the SimplifiedCallGraph \p SCG, and insert them into \p Fns.
-static void addAllDependencies(SimplifiedCallGraph &SCG, const Function &F,
-                               DenseSet<const Function *> &Fns) {
-  assert(!F.isDeclaration());
-  SmallVector<const Function *> WorkList({&F});
-  Fns.insert(&F);
-
-  while (!WorkList.empty()) {
-    const auto &CurFn = *WorkList.pop_back_val();
-    assert(!CurFn.isDeclaration());
-
-    // Walk the callees of CurFn recorded in SimplifiedCallGraph and
-    // add them to Fns, recursing transitively via the WorkList.
-    for (auto &SCGNode : *SCG.at(&CurFn)) {
-      auto *Callee = SCGNode->getFunction();
-      if (!Callee || Callee->isDeclaration())
-        continue;
-      if (Fns.insert(Callee).second)
-        WorkList.push_back(Callee);
-    }
-  }
-}
-
 /// The root function of the call graph, along with its transitive dependency
 /// closure and cumulative cost. Used by createWorkList to build the
 /// partitioning worklist and by doPartitioning for load-balanced
 /// bin-packing; it is the smallest unit allocated by doPartitioning.
 struct FunctionWithDependencies {
+  using CostType = InstructionCost::CostType;
+
+  /// Collects \p F and all non-declaration functions transitively called by
+  /// \p F into Dependencies, and computes TotalCost as the sum of the costs
+  /// of all collected functions per \p FnCosts.
   FunctionWithDependencies(SimplifiedCallGraph &SCG,
                            const DenseMap<const Function *, CostType> &FnCosts,
-                           const Function *F)
-      : F(F) {
-    addAllDependencies(SCG, *F, Dependencies);
-
-    for (const auto *Dep : Dependencies)
-      TotalCost += FnCosts.lookup(Dep);
-  }
+                           const Function *F);
 
   // The root function of the call graph.
   const Function *F = nullptr;
@@ -161,55 +153,76 @@ struct FunctionWithDependencies {
   CostType TotalCost = 0;
 };
 
-/// Splits a module into N linkable partitions by traversing its call graph,
+/// Splits a module into linkable partitions by traversing its call graph,
 /// so that each partition carries a self-consistent subset of functions
 /// (a root + its callees) and is balanced by IR-instruction cost. The
-/// resulting partitions can be optimized and codegen'd in parallel by the
-/// LTO backend and merged back into a single object.
+/// partitions can be optimized and codegen'd in parallel.
 ///
-/// Workflow (driven by SplitModule):
-///   1. externalize(): promote local symbols to external+hidden so they are
-///      visible across partitions. Unnamed entities get a stable name.
-///   2. calculateFunctionCosts(): compute per-function IR instruction counts.
-///   3. createWorkList(): walk SimplifiedCallGraph to discover call-graph roots
-///      and their transitive dependencies.
-///   4. doPartitioning(): greedily assign each root + dependencies to the
-///      least-loaded partition, balancing by accumulated cost.
-///   5. For each partition: CloneModule the original module filtered by
-///      ShouldCloneDefinition, then dealWithMpart downgrades duplicate
-///      external function definitions to available_externally and renames
-///      promoted locals to avoid duplicate symbols across partitions.
-///   6. Each partition bitcode is serialized to its own LLVMContext (via
-///      write+read) so partitions can be processed on concurrent threads
-///      without sharing LLVMContext state.
+/// calculateFunctionCosts() and createWorkList() run in the constructor;
+/// splitModule then:
+///   1. externalize(): promotes locals to external+hidden; unnamed entities
+///      get a stable name.
+///   2. sortWorkList(): sorts the worklist by accumulated cost.
+///   3. doPartitioning(): greedily assigns each root + dependencies to the
+///      least-loaded partition.
+///   4. Per partition: CloneModule, then dealWithMpart downgrades duplicate
+///      definitions to available_externally, erases unused declarations,
+///      and renames promoted locals with a common per-module suffix.
+///   5. Serializes each partition and re-parses it into a fresh
+///      caller-created LLVMContext on a worker thread.
 class SplitModuleCG {
 public:
+  /// Invoked once per partition on its worker thread, passing the partition
+  /// module parsed into a fresh context created by ContextCreationCallback.
   using ModuleCreationCallback =
       function_ref<void(std::unique_ptr<Module> MPart, unsigned PartitionId)>;
 
+  /// Factory creating the fresh LLVMContext each partition is parsed into
+  /// (LLVMContext cannot be shared across threads).
+  using ContextCreationCallback = function_ref<std::unique_ptr<LLVMContext>()>;
+
   /// Construct a SplitModuleCG over module \p M.
   ///
   /// \param M The module to partition. Must outlive the SplitModuleCG
-  ///          instance and any partitions emitted via SplitModule().
+  ///          instance and any partitions emitted via splitModule().
   /// \param LimitPartition Upper bound on the number of partitions to
   ///          produce. Pass 0 (the default) to derive the partition count
   ///          from the number of call-graph roots discovered in
-  ///          createWorkList (one root per partition at most). The actual
-  ///          partition count is finalized in the constructor.
+  ///          createWorkList, capped to the available hardware parallelism.
+  ///          The actual partition count is finalized in the constructor.
   SplitModuleCG(Module &M, unsigned LimitPartition = 0);
-  void SplitModule(ModuleCreationCallback ModuleCallback,
-                   const llvm::lto::Config &C);
+
+  /// Splits the module and invokes \p ModuleCallback once per partition from
+  /// the worker threads, using a fresh context per partition (see
+  /// ContextCreationCallback).
+  void splitModule(ModuleCreationCallback ModuleCallback,
+                   ContextCreationCallback MakeCtx);
 
 private:
-  unsigned N;
+  using CostType = InstructionCost::CostType;
+
+  /// Number of partitions to produce; finalized in the constructor.
+  unsigned NumPartitions;
   Module &M;
   CallGraph CG;
   std::unique_ptr<SimplifiedCallGraph> SCG;
+  /// Total IR-instruction cost of all non-declaration functions in M.
   CostType ModuleCost;
+  /// Call-graph roots discovered by createWorkList.
   DenseSet<const Function *> EntryFuncs;
+  /// Names of GVs already external before externalize; excluded from renaming.
   StringSet<> OriginalExternals;
+  /// Tracks functions that may be defined in multiple partitions so that
+  /// dealWithMpart can downgrade duplicates to available_externally:
+  ///   - Membership: the definition is safely downgradable (see
+  ///     canDowngradeToAvailableExternally); absent functions are untouched.
+  ///   - The mapped bool: true until the first partition holding the
+  ///     definition claims it (sets false) and keeps it; later partitions
+  ///     downgrade their copies to available_externally.
   DenseMap<const Function *, bool> ExternalFunction;
+  /// IR-instruction cost of each non-declaration function in M.
   DenseMap<const Function *, CostType> FuncsCosts;
+  /// Partitioning worklist built by createWorkList, sorted by TotalCost.
   SmallVector<FunctionWithDependencies> FWDWorkList;
 
   /// Compute the IR-instruction cost of every non-declaration function in M
@@ -218,12 +231,13 @@ class SplitModuleCG {
 
   /// Walk FWDWorkList in cost-sorted order and greedily assign each root and
   /// its dependencies to the partition with the lowest accumulated cost
-  /// (load-balanced bin-packing). Returns N partition sets, one per partition.
+  /// (load-balanced bin-packing). Returns one set per partition.
   std::vector<DenseSet<const Function *>> doPartitioning();
 
   /// Post-process a cloned partition \p MPart (partition index \p I):
   ///   - Downgrade duplicate definitions of originally-external functions to
   ///     available_externally.
+  ///   - Erase declarations that are no longer used.
   ///   - Rename promoted local symbols (now external, not in OriginalExternals)
   ///     to "name.llvm.<suffix>" to avoid duplicate symbols across partitions.
   void dealWithMpart(Module &MPart, unsigned I);
diff --git a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
index d204ab7523bbc..5b8fb3ae550c7 100644
--- a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
+++ b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
@@ -1,3 +1,16 @@
+//===- SplitModuleCG.cpp - Split a module by its call graph ---------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file implements the llvm::SplitModuleCG class, which splits a module
+// into partitions based on its call graph.
+//
+//===----------------------------------------------------------------------===//
+
 #include "llvm/Transforms/Utils/SplitModuleCG.h"
 #include "llvm/Bitcode/BitcodeReader.h"
 #include "llvm/Bitcode/BitcodeWriter.h"
@@ -5,23 +18,26 @@
 #include "llvm/IR/GlobalValue.h"
 #include "llvm/IR/Module.h"
 #include "llvm/IR/Value.h"
+#include "llvm/Support/CommandLine.h"
 #include "llvm/Support/MD5.h"
+#include "llvm/Support/ThreadPool.h"
 #include "llvm/Transforms/Utils/Cloning.h"
 #include "llvm/Transforms/Utils/ModuleUtils.h"
-#include <thread>
 using namespace llvm;
 
 #define DEBUG_TYPE "split-module-cg"
 
-namespace {
-
 static cl::opt<bool>
-    enablePrintSimplifiedCallGraph("enable-print-simplified-callgraph",
+    EnablePrintSimplifiedCallGraph("enable-print-simplified-callgraph",
                                    cl::Hidden, cl::init(false),
                                    cl::desc("print SimplifiedCallGraph"));
 
+namespace {
+
 using PartitionID = unsigned;
 
+} // namespace
+
 /// Returns whether duplicate definitions of \p F across partitions may be
 /// downgraded to available_externally. This is safe for external functions
 /// (either originally external or promoted by externalize), and for
@@ -35,14 +51,33 @@ static bool canDowngradeToAvailableExternally(const Function &F) {
           F.hasLinkOnceODRLinkage());
 }
 
-} // namespace
+/// Fallback for getUniqueModuleId when the module has no exportable symbols
+/// to seed a hash (e.g. every definition is in a comdat or has linkonce_odr
+/// linkage), in which case getUniqueModuleId returns "". Hashes the module
+/// identifier and the names of all named global values so that such modules
+/// can still be split with promoted locals renamed uniquely per module.
+static std::string computeFallbackSuffix(const Module &M) {
+  MD5 Md5;
+  Md5.update(M.getModuleIdentifier());
+  Md5.update(ArrayRef<uint8_t>{0});
+  for (const GlobalValue &GV : M.global_values()) {
+    if (!GV.hasName())
+      continue;
+    Md5.update(GV.getName());
+    Md5.update(ArrayRef<uint8_t>{0});
+  }
+  MD5::MD5Result R;
+  Md5.final(R);
+  SmallString<32> Str;
+  MD5::stringifyResult(R, Str);
+  return ("." + Str).str();
+}
 
 std::vector<DenseSet<const Function *>> SplitModuleCG::doPartitioning() {
   LLVM_DEBUG(dbgs() << "\n--Partitioning Starts--\n");
-  // Performs all of the partitioning work on M.
-  assert(N != 0 && "Partition count must be at least 1");
+  assert(NumPartitions != 0 && "Partition count must be at least 1");
   std::vector<DenseSet<const Function *>> Partitions;
-  Partitions.resize(N);
+  Partitions.resize(NumPartitions);
 
   auto ComparePartitions = [](const std::pair<PartitionID, CostType> &LHS,
                               const std::pair<PartitionID, CostType> &RHS) {
@@ -55,7 +90,7 @@ std::vector<DenseSet<const Function *>> SplitModuleCG::doPartitioning() {
   };
 
   std::vector<std::pair<PartitionID, CostType>> BalancingQueue;
-  for (unsigned I = 0; I < N; ++I)
+  for (unsigned I = 0; I < NumPartitions; ++I)
     BalancingQueue.emplace_back(I, 0);
 
   for (auto &CurFn : FWDWorkList) {
@@ -71,13 +106,9 @@ std::vector<DenseSet<const Function *>> SplitModuleCG::doPartitioning() {
       if (FnsInPart.insert(Dep).second)
         AddedCost += FuncsCosts.lookup(Dep);
 
-    // Update the balancing queue. We scan backwards because in the common
-    // case the target partition is at the end of the sorted queue.
-    for (auto &[QueuePID, Cost] : reverse(BalancingQueue)) {
-      if (QueuePID != PID)
-        continue;
-      Cost += AddedCost;
-    }
+    // Update the cost of the selected partition, which is the entry at the
+    // back of the sorted queue, before re-sorting.
+    BalancingQueue.back().second += AddedCost;
 
     sort(BalancingQueue, ComparePartitions);
   }
@@ -96,7 +127,11 @@ void SplitModuleCG::calculateFunctionCosts() {
       FnCost += std::distance(BB.begin(), BB.end());
     assert(FnCost != 0);
     FuncsCosts[&Fn] = FnCost;
-    assert((ModuleCost + FnCost) >= ModuleCost && "Overflow!");
+    // Signed overflow is UB, so perform the check in unsigned arithmetic,
+    // where wraparound is well-defined.
+    assert(static_cast<uint64_t>(ModuleCost) + static_cast<uint64_t>(FnCost) >=
+               static_cast<uint64_t>(ModuleCost) &&
+           "Overflow!");
     ModuleCost += FnCost;
   }
 }
@@ -121,27 +156,26 @@ void SplitModuleCG::dealWithMpart(Module &MPart, unsigned I) {
     }
   }
 
-  // Rename GlobalValues whose linkage was promoted from local to external,
+  // Erase declarations that are no longer used.
+  for (auto &GV : make_early_inc_range(MPart.global_values()))
+    if (GV.isDeclaration() && GV.use_empty())
+      GV.eraseFromParent();
+
+  // Rename GlobalValues whose linkage was promoted from internal to external,
   // to avoid duplicate symbols across partitions in ThinLTO. Use the naming
-  // convention "name.llvm.<suffix>" so the promoted local cannot clash with
+  // convention "name.llvm.<suffix>" so the promoted internal cannot clash with
   // an external that happens to share the same name. The suffix is derived
   // from the module via getUniqueModuleId, so it is consistent across all
   // partitions.
   std::string Suffix = getUniqueModuleId(&M);
+  if (Suffix.empty())
+    Suffix = computeFallbackSuffix(M);
   for (auto &GV : MPart.global_values()) {
-    // Only rename symbols that were promoted from local to external: skip
-    // those that are still local, and those that were already external in
+    // Only rename symbols that were promoted from internal to external: skip
+    // those that are still internal, and those that were already external in
     // the source module (recorded in OriginalExternals).
     if (GV.hasLocalLinkage() || OriginalExternals.contains(GV.getName()))
       continue;
-    // Skip declarations of functions that were not explicitly externalized
-    // (e.g. skipped by the hasOneUse check). Their definitions in other
-    // partitions remain internal and are not renamed, so declarations must
-    // keep the original name to stay consistent.
-    auto *Fn = dyn_cast<Function>(&GV);
-    if (Fn && Fn->isDeclaration() &&
-        !ExternalFunction.contains(M.getFunction(Fn->getName())))
-      continue;
     GV.setName((GV.getName() + ".llvm" + Suffix).str());
   }
 
@@ -154,6 +188,35 @@ void SplitModuleCG::dealWithMpart(Module &MPart, unsigned I) {
 #endif
 }
 
+FunctionWithDependencies::FunctionWithDependencies(
+    SimplifiedCallGraph &SCG,
+    const DenseMap<const Function *, CostType> &FnCosts, const Function *F)
+    : F(F) {
+  assert(!F->isDeclaration());
+
+  // Collect F and all non-declaration functions transitively called by F.
+  SmallVector<const Function *> WorkList({F});
+  Dependencies.insert(F);
+
+  while (!WorkList.empty()) {
+    const auto *CurFn = WorkList.pop_back_val();
+    assert(!CurFn->isDeclaration());
+
+    // Walk the callees of CurFn recorded in SimplifiedCallGraph and
+    // add them to Dependencies, recursing transitively via the WorkList.
+    for (auto &SCGNode : *SCG.at(CurFn)) {
+      auto *Callee = SCGNode->getFunction();
+      if (!Callee || Callee->isDeclaration())
+        continue;
+      if (Dependencies.insert(Callee).second)
+        WorkList.push_back(Callee);
+    }
+  }
+
+  for (const auto *Dep : Dependencies)
+    TotalCost += FnCosts.lookup(Dep);
+}
+
 void SplitModuleCG::createWorkList() {
   // First, find all the entry functions with an in-degree of 0
   // (i.e., those that are not called by any function).
@@ -164,15 +227,13 @@ void SplitModuleCG::createWorkList() {
   }
 
   // Second, find all the dependencies of each entry function.
-  for (auto *F : EntryFuncs) {
+  for (auto *F : EntryFuncs)
     FWDWorkList.emplace_back(*SCG, FuncsCosts, F);
-  }
 
   // Third, find all the functions that are not in the worklist.
   DenseSet<const Function *> SeenFunctions;
-  for (const auto &Fwd : FWDWorkList) {
+  for (const auto &Fwd : FWDWorkList)
     SeenFunctions.insert(Fwd.Dependencies.begin(), Fwd.Dependencies.end());
-  }
   for (auto &F : M) {
     // This function may be in a cycle, and therefore is not a dependency of
     // any root, which is treated as a root function here.
@@ -209,8 +270,8 @@ void SplitModuleCG::sortWorkList() {
 #endif
 }
 
-void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
-                                const llvm::lto::Config &C) {
+void SplitModuleCG::splitModule(ModuleCreationCallback ModuleCallback,
+                                ContextCreationCallback MakeCtx) {
   for (Function &F : M) {
     if (F.hasLocalLinkage() && F.hasOneUse() && !F.hasAddressTaken())
       continue;
@@ -226,13 +287,17 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
     GA.externalize();
   for (GlobalIFunc &GI : M.ifuncs())
     GI.externalize();
+  // TODO: The verifier requires an alias to point to a definition and an
+  // ifunc resolver to be a definition, but the aliasee/resolver may be
+  // assigned to a different partition than its alias/ifunc. Handling this
+  // is deferred to a follow-up patch.
 
   // Sort the worklist here, after all potential renaming has occurred.
   sortWorkList();
 
-  // Assign callgraphs into N partitions.
+  // Assign callgraphs into NumPartitions partitions.
   auto Partitions = doPartitioning();
-  assert(Partitions.size() == N);
+  assert(Partitions.size() == NumPartitions);
 
   auto ShouldCloneDefinition = [&](unsigned I, const GlobalValue *GV) {
     const auto &FnsInPart = Partitions[I];
@@ -245,15 +310,19 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
   };
 
   // TODO: Consider parallelizing the per-partition CloneModule call itself.
-  // Today the loop below serially clones M into N partitions in the main
-  // thread, then spawns N worker threads to run opt+codegen. If CloneModule
-  // becomes a bottleneck for large modules, the clones could be produced in
-  // parallel too — but that would require either per-thread LLVMContexts
-  // for the clone step or a thread-safe CloneModule, neither of which is
-  // straightforward.
-  std::vector<std::thread> Threads;
-  Threads.reserve(N);
-  for (unsigned I = 0; I < N; ++I) {
+  // Today the loop below serially clones M into NumPartitions partitions in
+  // the main thread, then enqueues the opt+codegen work on a thread pool. If
+  // CloneModule becomes a bottleneck for large modules, the clones could be
+  // produced in parallel too — but that would require either per-thread
+  // LLVMContexts for the clone step or a thread-safe CloneModule, neither of
+  // which is straightforward. dealWithMpart's handling of duplicate
+  // definitions is also order-dependent: the first partition processed keeps
+  // the real definition of an externalized function and later ones downgrade
+  // their copies to available_externally. Parallelizing would make that
+  // choice non-deterministic unless the owner partition were pre-assigned.
+  DefaultThreadPool SplitThreadPool(
+      heavyweight_hardware_concurrency(NumPartitions));
+  for (unsigned I = 0; I < NumPartitions; ++I) {
     ValueToValueMapTy VMap;
     std::unique_ptr<Module> MPart(
         CloneModule(M, VMap, [&](const GlobalValue *GV) {
@@ -273,24 +342,31 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
     raw_svector_ostream BCOS(BC);
     WriteBitcodeToFile(*MPart, BCOS);
     MPart.reset();
-    Threads.emplace_back(
+    SplitThreadPool.async(
         [&, I](SmallString<0> BC) {
-          llvm::lto::LTOLLVMContext Ctx(C);
+          // Each partition is parsed into its own context, created by the
+          // caller, as LLVMContext cannot be shared across threads.
+          std::unique_ptr<LLVMContext> Ctx = MakeCtx();
+          // Give each partition a distinct module name reflecting its index,
+          // so that diagnostics and debug output identify the partition.
+          std::string PartName = ("split-module-cg." + Twine(I)).str();
           Expected<std::unique_ptr<Module>> MOrErr =
-              parseBitcodeFile(MemoryBufferRef(BC.str(), "ld-temp.o"), Ctx);
+              parseBitcodeFile(MemoryBufferRef(BC.str(), PartName), *Ctx);
           BC = SmallString<0>();
           if (!MOrErr)
-            report_fatal_error("Failed to read bitcode");
+            report_fatal_error(MOrErr.takeError());
           ModuleCallback(std::move(MOrErr.get()), I);
         },
         std::move(BC));
   }
-  for (auto &T : Threads)
-    T.join();
+  // The inner lambda (which runs in a worker thread) captures our local
+  // variables, so we need to wait for the worker threads to terminate before
+  // we can leave the function scope.
+  SplitThreadPool.wait();
 }
 
 SplitModuleCG::SplitModuleCG(Module &M, unsigned LimitPartition)
-    : N(LimitPartition), M(M), CG(M) {
+    : NumPartitions(LimitPartition), M(M), CG(M) {
   // Track existing non-local symbols. This ensures that when we promote
   // internal symbols to external for partitioning, we can handle renaming
   // and avoid conflicts.
@@ -308,10 +384,20 @@ SplitModuleCG::SplitModuleCG(Module &M, unsigned LimitPartition)
   // subsequent module partitioning.
   createWorkList();
 
-  if (N == 0 || N > EntryFuncs.size())
-    N = EntryFuncs.size();
-  if (N == 0)
-    N = 1;
+  if (NumPartitions == 0) {
+    // Auto mode: one partition per call-graph root, but capped to the
+    // available hardware parallelism so that a module with a very large
+    // number of entry functions does not create an excessive number of
+    // partitions (and thus output objects).
+    unsigned MaxPartitions =
+        heavyweight_hardware_concurrency().compute_thread_count();
+    NumPartitions = std::min<unsigned>(EntryFuncs.size(), MaxPartitions);
+  } else if (NumPartitions > EntryFuncs.size()) {
+    // Do not create more partitions than there are call-graph roots.
+    NumPartitions = EntryFuncs.size();
+  }
+  if (NumPartitions == 0)
+    NumPartitions = 1;
 }
 
 SimplifiedCallGraph::SimplifiedCallGraph(CallGraph &CG) {
@@ -331,7 +417,7 @@ SimplifiedCallGraph::SimplifiedCallGraph(CallGraph &CG) {
     }
   }
 
-  if (enablePrintSimplifiedCallGraph)
+  if (EnablePrintSimplifiedCallGraph)
     print();
 }
 
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/alias-unsupported.ll b/llvm/test/tools/llvm-split/SplitModuleCG/alias-unsupported.ll
new file mode 100644
index 0000000000000..823e7943d6c91
--- /dev/null
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/alias-unsupported.ll
@@ -0,0 +1,28 @@
+; RUN: not llvm-split -enable-call-graph-split-module=true -j2 -o %t %s 2>&1 | FileCheck %s
+
+; Test the currently unsupported case of a GlobalAlias whose aliasee is
+; assigned to a different partition. Aliases are always placed in partition 0,
+; where the aliasee then only appears as a declaration, which the verifier
+; rejects ("Alias must point to a definition").
+;
+; TODO: A follow-up patch will keep an alias and its aliasee in the same
+; partition. Update this test to check the fixed behavior at that point.
+
+; CHECK: Alias must point to a definition
+
+ at alias_fn = alias void (), ptr @aliasee
+
+; @aliasee is an expensive call-graph root, so the cost-based partitioning
+; places it in partition 1, away from its alias in partition 0.
+define void @aliasee() {
+  call void @aliasee_helper()
+  ret void
+}
+
+define void @aliasee_helper() {
+  ret void
+}
+
+define void @root() {
+  ret void
+}
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/function-with-cycle.ll b/llvm/test/tools/llvm-split/SplitModuleCG/function-with-cycle.ll
index 0d31bd7ec5649..49f1aa0d4b91b 100644
--- a/llvm/test/tools/llvm-split/SplitModuleCG/function-with-cycle.ll
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/function-with-cycle.ll
@@ -5,15 +5,11 @@
 ; Test splitting when the call graph contains a cycle (foo -> call_foo -> foo),
 ; verifying that cycle members land in the same partition.
 
-; CHECK0-DAG: declare void @foo()
 ; CHECK0-DAG: define void @bar()
-; CHECK0-DAG: declare void @call_foo()
 ; CHECK0-DAG: define void @call_bar()
 
 ; CHECK1-DAG: define void @foo()
-; CHECK1-DAG: declare void @bar()
 ; CHECK1-DAG: define void @call_foo()
-; CHECK1-DAG: declare void @call_bar()
 
 define void @foo() {
 entry:
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/function.ll b/llvm/test/tools/llvm-split/SplitModuleCG/function.ll
index 86eb1f6c60d97..32750f234dfb5 100644
--- a/llvm/test/tools/llvm-split/SplitModuleCG/function.ll
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/function.ll
@@ -5,14 +5,11 @@
 ; Test basic call graph based module splitting — functions are grouped into
 ; partitions by their call relationships.
 
-; CHECK0-DAG: declare dso_local void @foo()
 ; CHECK0-DAG: define void @bar()
-; CHECK0-DAG: declare void @func_a()
 ; CHECK0-DAG: define void @func_b()
 ; CHECK1-DAG: define internal void @foo()
 ; CHECK1-DAG: define available_externally void @bar()
 ; CHECK1-DAG: define void @func_a()
-; CHECK1-DAG: declare void @func_b()
 
 define internal void @foo() {
 entry:
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/ifunc-unsupported.ll b/llvm/test/tools/llvm-split/SplitModuleCG/ifunc-unsupported.ll
new file mode 100644
index 0000000000000..55283f556ef5f
--- /dev/null
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/ifunc-unsupported.ll
@@ -0,0 +1,27 @@
+; RUN: not llvm-split -enable-call-graph-split-module=true -j2 -o %t %s 2>&1 | FileCheck %s
+
+; Test the currently unsupported case of a GlobalIFunc whose resolver is
+; assigned to a different partition. Ifuncs are always placed in partition 0,
+; where the resolver then only appears as a declaration, which the verifier
+; rejects ("IFunc resolver must be a definition").
+;
+; TODO: A follow-up patch will keep an ifunc and its resolver in the same
+; partition. Update this test to check the fixed behavior at that point.
+
+; CHECK: IFunc resolver must be a definition
+
+ at ifunc_fn = ifunc void (), ptr @resolver
+
+; @resolver is an expensive call-graph root, so the cost-based partitioning
+; places it in partition 1, away from its ifunc in partition 0.
+define ptr @resolver() {
+  ret ptr @target
+}
+
+define void @target() {
+  ret void
+}
+
+define void @root() {
+  ret void
+}
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/module-asm.ll b/llvm/test/tools/llvm-split/SplitModuleCG/module-asm.ll
new file mode 100644
index 0000000000000..dfb1dbf9ac836
--- /dev/null
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/module-asm.ll
@@ -0,0 +1,24 @@
+; RUN: llvm-split -enable-call-graph-split-module=true -j2 -o %t %s
+; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
+; RUN: llvm-dis -o - %t1 | FileCheck --check-prefix=CHECK1 %s
+
+; Test module-level inline asm.
+
+module asm ".globl __split_asm_marker"
+module asm "__split_asm_marker:"
+module asm ".long 42"
+
+; CHECK0: module asm
+; CHECK0:     ".globl __split_asm_marker"
+; CHECK1: module asm
+; CHECK1:     ".globl __split_asm_marker"
+
+define void @foo() {
+entry:
+  ret void
+}
+
+define void @bar() {
+entry:
+  ret void
+}
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/partition-cap.ll b/llvm/test/tools/llvm-split/SplitModuleCG/partition-cap.ll
index a0e7101ecad0c..77bf96b8b18b5 100644
--- a/llvm/test/tools/llvm-split/SplitModuleCG/partition-cap.ll
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/partition-cap.ll
@@ -1,9 +1,10 @@
 ; RUN: llvm-split -enable-call-graph-split-module=true -j10 -o %t %s
 ; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
 ; RUN: llvm-dis -o - %t1 | FileCheck --check-prefix=CHECK1 %s
+; RUN: not test -e %t2
 
 ; Test that partition count is capped to the number of entry functions
-; (-j10 but only 2 roots → 2 outputs).
+; (-j10 but only 2 roots → 2 outputs, no %t2 or beyond).
 
 ; CHECK0: define void @foo()
 ; CHECK1: define void @bar()
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll b/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll
index 6fb33d39e9093..b99f9883b180f 100644
--- a/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/unnamed.ll
@@ -1,14 +1,13 @@
 ; RUN: llvm-split -enable-call-graph-split-module=true -j2 -o %t %s
 ; RUN: llvm-dis -o - %t0 | FileCheck --check-prefix=CHECK0 %s
+; RUN: llvm-dis -o - %t1 | FileCheck --check-prefix=CHECK1 %s
 
 ; Test that an unnamed internal function (@0) is given a stable name and
 ; .llvm.<suffix> suffix after promotion.
 
-; CHECK0-DAG: declare hidden void @__llvm_unnamed.llvm.{{[0-9a-f]+}}()
 ; CHECK0-DAG: define hidden void @__llvm_unnamed.1.llvm.{{[0-9a-f]+}}()
 
 ; CHECK1-DAG: define hidden void @__llvm_unnamed.llvm.{{[0-9a-f]+}}()
-; CHECK1-DAG: declare hidden void @__llvm_unnamed.1.llvm.{{[0-9a-f]+}}()
 
 define internal void @0() {
   ret void
diff --git a/llvm/tools/llvm-split/llvm-split.cpp b/llvm/tools/llvm-split/llvm-split.cpp
index 429f86ddbd6a7..d2bfa671a82d7 100644
--- a/llvm/tools/llvm-split/llvm-split.cpp
+++ b/llvm/tools/llvm-split/llvm-split.cpp
@@ -80,7 +80,7 @@ static cl::opt<std::string>
          cl::value_desc("cpu"), cl::cat(SplitCategory));
 
 static cl::opt<bool> EnableCallGraphSplitModule(
-    "enable-call-graph-split-module", cl::Prefix, cl::init(false),
+    "enable-call-graph-split-module", cl::init(false),
     cl::desc("Split module using call graph"), cl::cat(SplitCategory));
 
 enum class SplitByCategoryType {
@@ -354,7 +354,9 @@ int main(int argc, char **argv) {
 
     llvm::lto::Config Config;
     SplitModuleCG SplitModuleCG(*M, NumOutputs);
-    SplitModuleCG.SplitModule(HandleModulePartCG, Config);
+    SplitModuleCG.splitModule(
+        HandleModulePartCG,
+        [&] { return std::make_unique<llvm::lto::LTOLLVMContext>(Config); });
     return 0;
   }
 

>From 8428106d476564bf048ad027e6c6efab5f547426 Mon Sep 17 00:00:00 2001
From: mmjjpp <maojiaping1 at huawei.com>
Date: Mon, 28 Sep 2026 12:12:30 +0000
Subject: [PATCH 13/13] [SplitModuleCG] Keep module inline asm only in the
 first partition

Emitting the module-level inline asm in every partition duplicates its
contents (and any symbols it defines) when the partition objects are
linked together. Remove it from all but partition 0, matching
llvm::SplitModule, and verify the behavior in module-asm.ll.
---
 llvm/lib/Transforms/Utils/SplitModuleCG.cpp            | 6 ++++++
 llvm/test/tools/llvm-split/SplitModuleCG/module-asm.ll | 4 ++--
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
index 5b8fb3ae550c7..389b8bbc395dd 100644
--- a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
+++ b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
@@ -329,6 +329,12 @@ void SplitModuleCG::splitModule(ModuleCreationCallback ModuleCallback,
           return ShouldCloneDefinition(I, GV);
         }));
 
+    // Keep the module inline asm only in the first partition: emitting it in
+    // every partition would duplicate its contents (and any symbols it
+    // defines) when the partition objects are linked together.
+    if (I != 0)
+      MPart->removeModuleInlineAsm();
+
     dealWithMpart(*MPart, I);
 
     // Serialize the cloned partition to bitcode and re-parse it inside the
diff --git a/llvm/test/tools/llvm-split/SplitModuleCG/module-asm.ll b/llvm/test/tools/llvm-split/SplitModuleCG/module-asm.ll
index dfb1dbf9ac836..d11b7b1107455 100644
--- a/llvm/test/tools/llvm-split/SplitModuleCG/module-asm.ll
+++ b/llvm/test/tools/llvm-split/SplitModuleCG/module-asm.ll
@@ -10,8 +10,8 @@ module asm ".long 42"
 
 ; CHECK0: module asm
 ; CHECK0:     ".globl __split_asm_marker"
-; CHECK1: module asm
-; CHECK1:     ".globl __split_asm_marker"
+; CHECK1-NOT: module asm
+; CHECK1-NOT:     ".globl __split_asm_marker"
 
 define void @foo() {
 entry:



More information about the llvm-commits mailing list