Author: Yaxun (Sam) Liu
Date: 2026-07-10T09:21:57-04:00
New Revision: f77162dd86a4abb102e6f9c717ab4e64497ef948

URL: 
https://github.com/llvm/llvm-project/commit/f77162dd86a4abb102e6f9c717ab4e64497ef948
DIFF: 
https://github.com/llvm/llvm-project/commit/f77162dd86a4abb102e6f9c717ab4e64497ef948.diff

LOG: [Clang][Driver] Parallelize device cc1 jobs for offload arches

[Clang][Driver] Parallelize device cc1 jobs for offload arches

Large offload builds can target many device architectures. A common
ROCm build can target around ten `--offload-arch=` values. For a large
single translation unit, the offload device cc1 work before
clang-linker-wrapper can become a build-time bottleneck. Those
per-architecture jobs are independent, but the driver ran them serially,
so `--offload-jobs=` only helped the later wrapper work.

Borrow the parallel job mechanism used by clang-linker-wrapper for the
pre-wrapper device cc1 jobs. Device job construction marks eligible
compile and backend jobs with an offload parallel group. The generic
executor only consumes that opt-in metadata for adjacent device jobs
with distinct bound architectures.

The parallel path is disabled for driver-side output modes and callbacks
so existing serial output handling is preserved. This is NFC for
non-offload compilation.

Added: 
    clang/test/Driver/offload-parallel-device-cc1.cu

Modified: 
    clang/include/clang/Driver/CommonArgs.h
    clang/include/clang/Driver/Job.h
    clang/lib/Driver/Compilation.cpp
    clang/lib/Driver/Driver.cpp
    clang/lib/Driver/ToolChains/Clang.cpp
    clang/lib/Driver/ToolChains/CommonArgs.cpp

Removed: 
    


################################################################################
diff  --git a/clang/include/clang/Driver/CommonArgs.h 
b/clang/include/clang/Driver/CommonArgs.h
index 14a83ff7beae4..29d170eaaa17c 100644
--- a/clang/include/clang/Driver/CommonArgs.h
+++ b/clang/include/clang/Driver/CommonArgs.h
@@ -24,6 +24,19 @@ namespace clang {
 namespace driver {
 namespace tools {
 
+struct OffloadJobsOpt {
+  enum class Kind { Missing, Invalid, Jobserver, Fixed };
+
+  Kind K = Kind::Missing;
+  llvm::opt::Arg *A = nullptr;
+  StringRef Value;
+  unsigned NumThreads = 0;
+
+  bool isValid() const { return K == Kind::Jobserver || K == Kind::Fixed; }
+};
+
+OffloadJobsOpt parseOffloadJobs(const llvm::opt::ArgList &Args);
+
 void addPathIfExists(const Driver &D, const Twine &Path,
                      ToolChain::path_list &Paths);
 

diff  --git a/clang/include/clang/Driver/Job.h 
b/clang/include/clang/Driver/Job.h
index 56a147e717237..03779be5b5a6a 100644
--- a/clang/include/clang/Driver/Job.h
+++ b/clang/include/clang/Driver/Job.h
@@ -151,10 +151,13 @@ class Command {
   /// Information on executable run provided by OS.
   mutable std::optional<llvm::sys::ProcessStatistics> ProcStat;
 
-  /// The bound architecture for this command (e.g. "arm64", "x86_64").
-  /// Non-empty only for Darwin multi-arch builds.
+  /// The bound architecture for this command (e.g. "arm64", "gfx90a").
   std::string BoundArchStr;
 
+  /// Non-empty when this command may run in parallel with adjacent offload
+  /// device commands from the same group.
+  std::string OffloadDeviceParallelJobGroup;
+
   /// When a response file is needed, we try to put most arguments in an
   /// exclusive file, while others remains as regular command line arguments.
   /// This functions fills a vector with the regular command line arguments,
@@ -199,6 +202,13 @@ class Command {
   BoundArch getBoundArch() const { return BoundArch(BoundArchStr); }
   void setBoundArch(BoundArch BA) { BoundArchStr = BA.ArchName.str(); }
 
+  StringRef getOffloadDeviceParallelJobGroup() const {
+    return OffloadDeviceParallelJobGroup;
+  }
+  void setOffloadDeviceParallelJobGroup(StringRef Group) {
+    OffloadDeviceParallelJobGroup = Group.str();
+  }
+
   /// Returns the kind of response file supported by the current invocation.
   const ResponseFileSupport &getResponseFileSupport() {
     return ResponseSupport;

diff  --git a/clang/lib/Driver/Compilation.cpp 
b/clang/lib/Driver/Compilation.cpp
index 377ac7e2ad43e..c81c4445a29f9 100644
--- a/clang/lib/Driver/Compilation.cpp
+++ b/clang/lib/Driver/Compilation.cpp
@@ -9,18 +9,24 @@
 #include "clang/Driver/Compilation.h"
 #include "clang/Basic/LLVM.h"
 #include "clang/Driver/Action.h"
+#include "clang/Driver/CommonArgs.h"
 #include "clang/Driver/Driver.h"
 #include "clang/Driver/Job.h"
 #include "clang/Driver/ToolChain.h"
 #include "clang/Driver/Util.h"
 #include "clang/Options/Options.h"
+#include "llvm/ADT/STLExtras.h"
 #include "llvm/Option/ArgList.h"
 #include "llvm/Option/OptSpecifier.h"
 #include "llvm/Option/Option.h"
 #include "llvm/Support/FileSystem.h"
+#include "llvm/Support/ThreadPool.h"
+#include "llvm/Support/Threading.h"
 #include "llvm/Support/raw_ostream.h"
 #include "llvm/TargetParser/Triple.h"
+#include <algorithm>
 #include <cassert>
+#include <optional>
 #include <string>
 #include <system_error>
 #include <utility>
@@ -232,6 +238,130 @@ static bool ActionFailed(const Action *A,
   return false;
 }
 
+static bool ActionDependsOn(const Action *A, const Action *Other) {
+  return A == Other || llvm::any_of(A->inputs(), [&](const Action *Input) {
+           return ActionDependsOn(Input, Other);
+         });
+}
+
+static bool ActionsAreIndependent(const Action *A, const Action *B) {
+  return !ActionDependsOn(A, B) && !ActionDependsOn(B, A);
+}
+
+static bool CanRunInParallelOffloadJobGroup(const Command &Job) {
+  return !Job.InProcess && !Job.PrintInputFilenames &&
+         !Job.getBoundArch().empty() &&
+         !Job.getOffloadDeviceParallelJobGroup().empty();
+}
+
+static bool SameParallelOffloadJobGroup(const Command &A, const Command &B) {
+  return A.getOffloadDeviceParallelJobGroup() ==
+         B.getOffloadDeviceParallelJobGroup();
+}
+
+static bool HasDistinctBoundArch(const Command &Candidate,
+                                 ArrayRef<const Command *> Jobs) {
+  BoundArch CandidateArch = Candidate.getBoundArch();
+  return llvm::none_of(Jobs, [&](const Command *Job) {
+    return Job->getBoundArch() == CandidateArch;
+  });
+}
+
+static std::optional<llvm::ThreadPoolStrategy>
+getParallelOffloadJobsStrategy(const ArgList &Args, unsigned NumJobs) {
+  if (NumJobs < 2)
+    return std::nullopt;
+
+  auto OffloadJobs = tools::parseOffloadJobs(Args);
+  if (!OffloadJobs.isValid())
+    return std::nullopt;
+
+  if (OffloadJobs.K == tools::OffloadJobsOpt::Kind::Jobserver)
+    return llvm::jobserver_concurrency();
+
+  if (OffloadJobs.NumThreads < 2)
+    return std::nullopt;
+
+  return llvm::hardware_concurrency(std::min(OffloadJobs.NumThreads, NumJobs));
+}
+
+struct ParallelJobResult {
+  int Res = 0;
+  bool ExecutionFailed = false;
+  std::string Error;
+};
+
+struct ParallelOffloadJobGroupResult {
+  size_t NumJobs = 0;
+};
+
+static std::optional<ParallelOffloadJobGroupResult>
+tryExecuteParallelOffloadJobGroup(const Driver &D, const ArgList &Args,
+                                  ArrayRef<std::optional<StringRef>> Redirects,
+                                  const JobList::list_type &JobStorage,
+                                  size_t StartIndex,
+                                  FailingCommandList &FailingCommands) {
+  const Command &Job = *JobStorage[StartIndex];
+  if (!CanRunInParallelOffloadJobGroup(Job))
+    return std::nullopt;
+
+  SmallVector<const Command *, 4> ParallelJobs;
+  for (size_t I = StartIndex; I < JobStorage.size(); ++I) {
+    const Command &Candidate = *JobStorage[I];
+    if (ActionFailed(&Candidate.getSource(), FailingCommands))
+      break;
+
+    if (!CanRunInParallelOffloadJobGroup(Candidate))
+      break;
+
+    if (!SameParallelOffloadJobGroup(Job, Candidate))
+      break;
+
+    if (!HasDistinctBoundArch(Candidate, ParallelJobs))
+      break;
+
+    if (!llvm::all_of(ParallelJobs, [&](const Command *Other) {
+          return ActionsAreIndependent(&Candidate.getSource(),
+                                       &Other->getSource());
+        }))
+      break;
+
+    ParallelJobs.push_back(&Candidate);
+  }
+
+  std::optional<llvm::ThreadPoolStrategy> Strategy =
+      getParallelOffloadJobsStrategy(Args, ParallelJobs.size());
+  if (!Strategy)
+    return std::nullopt;
+
+  SmallVector<ParallelJobResult, 4> Results(ParallelJobs.size());
+  llvm::DefaultThreadPool Pool(*Strategy);
+  for (auto IndexedJob : llvm::enumerate(ParallelJobs)) {
+    size_t Index = IndexedJob.index();
+    const Command *ParallelJob = IndexedJob.value();
+    Pool.async([&, Index, ParallelJob] {
+      Results[Index].Res = ParallelJob->Execute(
+          Redirects, &Results[Index].Error, &Results[Index].ExecutionFailed);
+    });
+  }
+  Pool.wait();
+
+  for (auto [Index, ParallelJob] : llvm::enumerate(ParallelJobs)) {
+    ParallelJobResult &Result = Results[Index];
+    if (!Result.Error.empty()) {
+      assert(Result.Res && "Error string set with 0 result code!");
+      D.Diag(diag::err_drv_command_failure) << Result.Error;
+    }
+
+    if (Result.Res) {
+      FailingCommands.push_back(
+          std::make_pair(Result.ExecutionFailed ? 1 : Result.Res, 
ParallelJob));
+    }
+  }
+
+  return ParallelOffloadJobGroupResult{ParallelJobs.size()};
+}
+
 void Compilation::ExecuteJobs(const JobList &Jobs,
                               FailingCommandList &FailingCommands,
                               bool LogOnly) const {
@@ -239,9 +369,30 @@ void Compilation::ExecuteJobs(const JobList &Jobs,
   // inputs on the command line even one of them failed.
   // In all but CLMode, execute all the jobs unless the necessary inputs for 
the
   // job is missing due to previous failures.
-  for (const auto &Job : Jobs) {
-    if (ActionFailed(&Job.getSource(), FailingCommands))
+  bool CanRunJobsInParallel =
+      !LogOnly && !getDriver().CCPrintOptions &&
+      !getDriver().CCPrintProcessStats && !getDriver().CCGenDiagnostics &&
+      !getDriver().IsCLMode() && !getArgs().hasArg(options::OPT_v) &&
+      Redirects.empty() && !PostCallback;
+
+  const auto &JobStorage = Jobs.getJobs();
+  for (size_t I = 0; I < JobStorage.size();) {
+    const auto &Job = *JobStorage[I];
+    if (ActionFailed(&Job.getSource(), FailingCommands)) {
+      ++I;
       continue;
+    }
+
+    if (CanRunJobsInParallel) {
+      if (std::optional<ParallelOffloadJobGroupResult> Result =
+              tryExecuteParallelOffloadJobGroup(getDriver(), getArgs(),
+                                                Redirects, JobStorage, I,
+                                                FailingCommands)) {
+        I += Result->NumJobs;
+        continue;
+      }
+    }
+
     const Command *FailingCommand = nullptr;
     if (int Res = ExecuteCommand(Job, FailingCommand, LogOnly)) {
       FailingCommands.push_back(std::make_pair(Res, FailingCommand));
@@ -249,6 +400,7 @@ void Compilation::ExecuteJobs(const JobList &Jobs,
       if (TheDriver.IsCLMode())
         return;
     }
+    ++I;
   }
 }
 

diff  --git a/clang/lib/Driver/Driver.cpp b/clang/lib/Driver/Driver.cpp
index 816088b9d7754..1fea1b68069bf 100644
--- a/clang/lib/Driver/Driver.cpp
+++ b/clang/lib/Driver/Driver.cpp
@@ -58,6 +58,7 @@
 #include "clang/Basic/Version.h"
 #include "clang/Config/config.h"
 #include "clang/Driver/Action.h"
+#include "clang/Driver/CommonArgs.h"
 #include "clang/Driver/Compilation.h"
 #include "clang/Driver/InputInfo.h"
 #include "clang/Driver/Job.h"
@@ -5417,6 +5418,67 @@ Action *Driver::ConstructPhaseAction(
   llvm_unreachable("invalid phase in ConstructPhaseAction");
 }
 
+static bool isOffloadDeviceCC1JobCandidate(Command &Job) {
+  const Action &Source = Job.getSource();
+  if (!isa<CompileJobAction>(Source) && !isa<BackendJobAction>(Source))
+    return false;
+
+  if (Job.getBoundArch().empty() && !Source.getOffloadingArch().empty())
+    Job.setBoundArch(Source.getOffloadingArch());
+
+  if (Job.getBoundArch().empty())
+    return false;
+
+  if (StringRef(Job.getCreator().getName()) != "clang")
+    return false;
+
+  Action::OffloadKind OKind = Source.getOffloadingDeviceKind();
+  if (OKind != Action::OFK_None && OKind != Action::OFK_Host)
+    return true;
+
+  const llvm::Triple &Triple = Job.getCreator().getToolChain().getTriple();
+  return Triple.isAMDGPU() || Triple.isNVPTX() || Triple.isSPIROrSPIRV();
+}
+
+static std::string getOffloadDeviceCC1ParallelJobGroup(const Command &Job) {
+  const Action &Source = Job.getSource();
+  // This key groups device cc1 jobs that can run in parallel. Jobs may 
diff er
+  // by offload arch, but must have the same offload kind, target triple,
+  // action kind, and output type. For example, HIP compile jobs for gfx900 and
+  // gfx906 can share a group, but HIP and OpenMP jobs cannot.
+  return (Twine(Action::GetOffloadKindName(Source.getOffloadingDeviceKind())) +
+          ":" + Job.getCreator().getToolChain().getTripleString() + ":" +
+          Source.getClassName() + ":" + types::getTypeName(Source.getType()))
+      .str();
+}
+
+static void claimAndDiagnoseOffloadJobs(const Driver &D, const ArgList &Args) {
+  auto OffloadJobs = tools::parseOffloadJobs(Args);
+  if (!OffloadJobs.A)
+    return;
+
+  if (!OffloadJobs.isValid())
+    D.Diag(diag::err_drv_invalid_int_value)
+        << OffloadJobs.A->getAsString(Args) << OffloadJobs.Value;
+
+  OffloadJobs.A->claim();
+}
+
+static void markOffloadDeviceCC1JobsForParallelExecution(Compilation &C) {
+  bool FoundCandidate = false;
+  for (auto &Job : C.getJobs()) {
+    if (!isOffloadDeviceCC1JobCandidate(Job))
+      continue;
+
+    Job.setOffloadDeviceParallelJobGroup(
+        getOffloadDeviceCC1ParallelJobGroup(Job));
+    FoundCandidate = true;
+  }
+
+  if (FoundCandidate)
+    claimAndDiagnoseOffloadJobs(C.getDriver(), C.getArgs());
+}
+
 void Driver::BuildJobs(Compilation &C) const {
   llvm::PrettyStackTraceString CrashInfo("Building compilation jobs");
 
@@ -5517,6 +5579,8 @@ void Driver::BuildJobs(Compilation &C) const {
     for (auto &J : C.getJobs())
       J.InProcess = false;
 
+  markOffloadDeviceCC1JobsForParallelExecution(C);
+
   if (CCPrintProcessStats) {
     C.setPostCallback([=](const Command &Cmd, int Res) {
       std::optional<llvm::sys::ProcessStatistics> ProcStat =

diff  --git a/clang/lib/Driver/ToolChains/Clang.cpp 
b/clang/lib/Driver/ToolChains/Clang.cpp
index a30b01a675b99..39eca94556049 100644
--- a/clang/lib/Driver/ToolChains/Clang.cpp
+++ b/clang/lib/Driver/ToolChains/Clang.cpp
@@ -10047,20 +10047,16 @@ void LinkerWrapper::ConstructJob(Compilation &C, 
const JobAction &JA,
 
   addOffloadCompressArgs(Args, CmdArgs);
 
-  if (Arg *A = Args.getLastArg(options::OPT_offload_jobs_EQ)) {
-    StringRef Val = A->getValue();
-
-    if (Val.equals_insensitive("jobserver"))
+  OffloadJobsOpt OffloadJobs = parseOffloadJobs(Args);
+  if (OffloadJobs.A) {
+    if (OffloadJobs.K == OffloadJobsOpt::Kind::Jobserver) {
       CmdArgs.push_back(Args.MakeArgString("--wrapper-jobs=jobserver"));
-    else {
-      int NumThreads;
-      if (Val.getAsInteger(10, NumThreads) || NumThreads <= 0) {
-        C.getDriver().Diag(diag::err_drv_invalid_int_value)
-            << A->getAsString(Args) << Val;
-      } else {
-        CmdArgs.push_back(
-            Args.MakeArgString("--wrapper-jobs=" + Twine(NumThreads)));
-      }
+    } else if (OffloadJobs.K == OffloadJobsOpt::Kind::Fixed) {
+      CmdArgs.push_back(Args.MakeArgString("--wrapper-jobs=" +
+                                           Twine(OffloadJobs.NumThreads)));
+    } else if (!OffloadJobs.A->isClaimed()) {
+      C.getDriver().Diag(diag::err_drv_invalid_int_value)
+          << OffloadJobs.A->getAsString(Args) << OffloadJobs.Value;
     }
   }
 

diff  --git a/clang/lib/Driver/ToolChains/CommonArgs.cpp 
b/clang/lib/Driver/ToolChains/CommonArgs.cpp
index 6a52f026598d1..96ebc94b47434 100644
--- a/clang/lib/Driver/ToolChains/CommonArgs.cpp
+++ b/clang/lib/Driver/ToolChains/CommonArgs.cpp
@@ -68,6 +68,22 @@ using namespace clang::driver::tools;
 using namespace clang;
 using namespace llvm::opt;
 
+OffloadJobsOpt tools::parseOffloadJobs(const ArgList &Args) {
+  Arg *A = Args.getLastArg(options::OPT_offload_jobs_EQ);
+  if (!A)
+    return {};
+
+  StringRef Val = A->getValue();
+  if (Val.equals_insensitive("jobserver"))
+    return {OffloadJobsOpt::Kind::Jobserver, A, Val};
+
+  int NumThreads;
+  if (Val.getAsInteger(10, NumThreads) || NumThreads <= 0)
+    return {OffloadJobsOpt::Kind::Invalid, A, Val};
+
+  return {OffloadJobsOpt::Kind::Fixed, A, Val, unsigned(NumThreads)};
+}
+
 static bool useFramePointerForTargetByDefault(const llvm::opt::ArgList &Args,
                                               const llvm::Triple &Triple) {
   if (Args.hasArg(options::OPT_pg) && !Args.hasArg(options::OPT_mfentry))

diff  --git a/clang/test/Driver/offload-parallel-device-cc1.cu 
b/clang/test/Driver/offload-parallel-device-cc1.cu
new file mode 100644
index 0000000000000..b3259c381f272
--- /dev/null
+++ b/clang/test/Driver/offload-parallel-device-cc1.cu
@@ -0,0 +1,21 @@
+// REQUIRES: x86-registered-target, amdgpu-registered-target
+// REQUIRES: nvptx-registered-target, lld
+
+// RUN: %clang -x hip --target=x86_64-unknown-linux-gnu \
+// RUN:   -nostdinc -nogpuinc -nohipwrapperinc -nogpulib \
+// RUN:   --offload-arch=gfx900 --offload-arch=gfx906 --offload-jobs=2 \
+// RUN:   -O3 -c %s -o %t.o
+
+// RUN: %clang -x cuda --target=x86_64-unknown-linux-gnu \
+// RUN:   -nocudainc -nocudalib \
+// RUN:   --cuda-gpu-arch=sm_70 --cuda-gpu-arch=sm_80 --offload-jobs=2 \
+// RUN:   --cuda-device-only -S -Werror %s
+
+// RUN: not %clang -x cuda --target=x86_64-unknown-linux-gnu \
+// RUN:   -nocudainc -nocudalib \
+// RUN:   --cuda-gpu-arch=sm_70 --cuda-gpu-arch=sm_80 --offload-jobs=0x4 \
+// RUN:   --cuda-device-only -S %s 2>&1 | FileCheck -check-prefix=INVJOBS %s
+// INVJOBS: clang: error: invalid integral value '0x4' in '--offload-jobs=0x4'
+
+// Empty source file. RUN lines are execution smoke tests for the driver
+// path that runs independent offload device cc1 jobs through --offload-jobs.


        
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to