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

via cfe-commits cfe-commits at lists.llvm.org
Sun Sep 13 19:50:11 PDT 2026


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

>From c098f6e0d46cbd41cccb0dd271a90a3b80da529b 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/11] [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 00000000000000..e60c4e931d40c7
--- /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 69bc3fdae6c578..cea94b41159dfb 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 103221325f9129..5f390fad7ace85 100644
--- a/llvm/lib/Transforms/Utils/CMakeLists.txt
+++ b/llvm/lib/Transforms/Utils/CMakeLists.txt
@@ -92,6 +92,7 @@ add_llvm_component_library(LLVMTransformUtils
   SplitModule.cpp
   SplitModuleByCategory.cpp
   SplitModuleCommon.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 00000000000000..9f57cb3ed566e4
--- /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 5687abef1bae418db398639e3f4cef29adeb1ae2 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/11] [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 e60c4e931d40c7..956a1ea8030fea 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 cea94b41159dfb..dda370b333b4ac 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 9f57cb3ed566e4..debdddfb790415 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 247acf3f18783d8f0871dc29ae9825efd6dff6df 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/11] [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 00000000000000..6c51141a9ad852
--- /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 00000000000000..f2fc8c03c922a0
--- /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 00000000000000..ddf5bb5c3dff32
--- /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 00000000000000..5c3ced3e682af5
--- /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 00000000000000..fdfdf910a34989
--- /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 00000000000000..73f7079669c555
--- /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 4ead6fd4b88be8..9294ce1e4c9f1b 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 6779c5a3fb003acd42be89b643dcee3c74a1d218 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/11] [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 956a1ea8030fea..9836376b94a82f 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 dda370b333b4ac..5fa96214777843 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 debdddfb790415..c50111204e1f01 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 b083f1706612c989fafe7fdde704a25273ae3370 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/11] [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 5fa96214777843..3d607d82f1402e 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 6c51141a9ad852..00000000000000
--- 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 263f7b585c9999cdb0684cda9a8a4edb2697e2f7 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/11] [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 00000000000000..b3cf7081ee2e0a
--- /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 00000000000000..0725fe49f3e6c7
--- /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 3d607d82f1402e..acab52023f89ce 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 fa0c23f0344f616408a8964ae51e4cf8257b6593 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/11] [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 b3cf7081ee2e0a..00000000000000
--- 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 0725fe49f3e6c7..00000000000000
--- 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 acab52023f89ce..69bc3fdae6c578 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 8d9524ec67d04c311c957dac8942620eedd22e45 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/11] [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 9836376b94a82f..b6c1b7e3303e5e 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 c50111204e1f01..16032a3336f1d4 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 9294ce1e4c9f1b..69c35e5f0d8afb 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 63c75a2200387cf4170875b088543438e0e07ed7 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/11] [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 b6c1b7e3303e5e..4f45fb82fe501e 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 16032a3336f1d4..ca924215f1a14e 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 f2fc8c03c922a0..0d31bd7ec56495 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 ddf5bb5c3dff32..86eb1f6c60d975 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 5c3ced3e682af5..a0e7101ecad0c9 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 00000000000000..3dd6ae8ed95274
--- /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 fdfdf910a34989..03b1454c0e2a83 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 73f7079669c555..228294c2ef4751 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 69c35e5f0d8afb..194248b1ea6030 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 edaaad5a3b1f300e7e1f336ddd22d2f3f2efbfa4 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/11] [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 4f45fb82fe501e..f07326773fcc9a 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 ca924215f1a14e..c5e573ef9f1422 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 00000000000000..bdd1ec641977ff
--- /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 194248b1ea6030..429f86ddbd6a7c 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 5c5fdb23f54f943fd50f25bebc92892b7f745382 Mon Sep 17 00:00:00 2001
From: mmjjpp <maojiaping1 at huawei.com>
Date: Mon, 14 Sep 2026 10:47:39 +0800
Subject: [PATCH 11/11] [SplitModuleCG] Use shared externalizeGlobal from
 SplitModuleCommon

---
 llvm/lib/Transforms/Utils/SplitModuleCG.cpp | 21 +++++----------------
 1 file changed, 5 insertions(+), 16 deletions(-)

diff --git a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
index c5e573ef9f1422..c452e602a328c1 100644
--- a/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
+++ b/llvm/lib/Transforms/Utils/SplitModuleCG.cpp
@@ -8,6 +8,7 @@
 #include "llvm/Support/MD5.h"
 #include "llvm/Transforms/Utils/Cloning.h"
 #include "llvm/Transforms/Utils/ModuleUtils.h"
+#include "llvm/Transforms/Utils/SplitModuleCommon.h"
 #include <thread>
 using namespace llvm;
 
@@ -22,18 +23,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
@@ -224,18 +213,18 @@ void SplitModuleCG::SplitModule(ModuleCreationCallback ModuleCallback,
   for (Function &F : M) {
     if (F.hasLocalLinkage() && F.hasOneUse() && !F.hasAddressTaken())
       continue;
-    externalize(&F);
+    externalizeGlobal(F);
     // Record functions that may be defined in multiple partitions so that
     // dealWithMpart can downgrade duplicates to available_externally.
     if (canDowngradeToAvailableExternally(F))
       externalFunction[&F] = true;
   }
   for (GlobalVariable &GV : M.globals())
-    externalize(&GV);
+    externalizeGlobal(GV);
   for (GlobalAlias &GA : M.aliases())
-    externalize(&GA);
+    externalizeGlobal(GA);
   for (GlobalIFunc &GI : M.ifuncs())
-    externalize(&GI);
+    externalizeGlobal(GI);
 
   // Assign callgraphs into N partitions.
   auto Partitions = doPartitioning();



More information about the cfe-commits mailing list