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
