https://github.com/yxsamliu updated 
https://github.com/llvm/llvm-project/pull/211675

>From 63f63083dc448731be8c6417f9314c524e8ff09c Mon Sep 17 00:00:00 2001
From: "Yaxun (Sam) Liu" <[email protected]>
Date: Tue, 28 Jul 2026 12:56:50 -0400
Subject: [PATCH] [HIP] Use linker wrapper for device-only code object links

HIP device-only code object builds used a separate AMDGPU linker path.
This bypassed compiler options and runtime library handling in
clang-linker-wrapper, including the device profile runtime.

Package device inputs and route final links through
clang-linker-wrapper. Default output uses the existing fat binary mode.
Add a raw image mode for `--no-gpu-bundle-output`, and keep LTO enabled
when the device profile runtime is required.
---
 clang/include/clang/Driver/Action.h           |  21 +++
 clang/include/clang/Driver/Compilation.h      |  15 +-
 clang/include/clang/Driver/Driver.h           |   6 +-
 clang/include/clang/Driver/Job.h              |   7 +
 clang/include/clang/Driver/Util.h             |   4 +
 clang/lib/Driver/Compilation.cpp              |   8 +-
 clang/lib/Driver/Driver.cpp                   | 155 ++++++++++++------
 clang/lib/Driver/ToolChains/AMDGPU.cpp        |   1 +
 clang/lib/Driver/ToolChains/Clang.cpp         |  77 ++++++++-
 clang/lib/Driver/ToolChains/Clang.h           |  10 ++
 clang/test/Driver/hip-binding.hip             |  33 +++-
 clang/test/Driver/hip-phases.hip              |  18 +-
 .../test/Driver/hip-profile-rocm-runtime.hip  |  60 ++++++-
 .../test/Driver/hip-toolchain-device-only.hip |  18 +-
 .../linker-wrapper-hip-no-rdc.c               |  69 ++++++++
 .../ClangLinkerWrapper.cpp                    |  98 ++++++++---
 .../clang-linker-wrapper/LinkerWrapperOpts.td |  10 ++
 17 files changed, 494 insertions(+), 116 deletions(-)

diff --git a/clang/include/clang/Driver/Action.h 
b/clang/include/clang/Driver/Action.h
index 306db1be15910..a943e03945742 100644
--- a/clang/include/clang/Driver/Action.h
+++ b/clang/include/clang/Driver/Action.h
@@ -650,9 +650,30 @@ class OffloadPackagerJobAction : public JobAction {
 class LinkerWrapperJobAction : public JobAction {
   void anchor() override;
 
+public:
+  struct DeviceOutputInfo final {
+    const ToolChain *DeviceToolChain = nullptr;
+    BoundArch DeviceBoundArch;
+
+    DeviceOutputInfo(const ToolChain *DeviceToolChain,
+                     BoundArch DeviceBoundArch)
+        : DeviceToolChain(DeviceToolChain), DeviceBoundArch(DeviceBoundArch) {}
+  };
+
+private:
+  SmallVector<DeviceOutputInfo, 4> DeviceOutputInfoArray;
+
 public:
   LinkerWrapperJobAction(ActionList &Inputs, types::ID Type);
 
+  void registerDeviceOutputInfo(const ToolChain *TC, BoundArch BA) {
+    DeviceOutputInfoArray.emplace_back(TC, BA);
+  }
+
+  ArrayRef<DeviceOutputInfo> getDeviceOutputInfo() const {
+    return DeviceOutputInfoArray;
+  }
+
   static bool classof(const Action *A) {
     return A->getKind() == LinkerWrapperJobClass;
   }
diff --git a/clang/include/clang/Driver/Compilation.h 
b/clang/include/clang/Driver/Compilation.h
index 825806b6cfe33..01eff0af7852f 100644
--- a/clang/include/clang/Driver/Compilation.h
+++ b/clang/include/clang/Driver/Compilation.h
@@ -101,11 +101,11 @@ class Compilation {
   llvm::opt::ArgStringList TempFiles;
 
   /// Result files which should be removed on failure.
-  ArgStringMap ResultFiles;
+  ActionFileMap ResultFiles;
 
   /// Result files which are generated correctly on failure, and which should
   /// only be removed if we crash.
-  ArgStringMap FailureResultFiles;
+  ActionFileMap FailureResultFiles;
 
   /// -ftime-trace result files.
   ArgStringMap TimeTraceFiles;
@@ -227,9 +227,9 @@ class Compilation {
   llvm::opt::ArgStringList &getTempFiles() { return TempFiles; }
   const llvm::opt::ArgStringList &getTempFiles() const { return TempFiles; }
 
-  const ArgStringMap &getResultFiles() const { return ResultFiles; }
+  const ActionFileMap &getResultFiles() const { return ResultFiles; }
 
-  const ArgStringMap &getFailureResultFiles() const {
+  const ActionFileMap &getFailureResultFiles() const {
     return FailureResultFiles;
   }
 
@@ -266,14 +266,14 @@ class Compilation {
   /// addResultFile - Add a file to remove on failure, and returns its
   /// argument.
   const char *addResultFile(const char *Name, const JobAction *JA) {
-    ResultFiles[JA] = Name;
+    ResultFiles[JA].push_back(Name);
     return Name;
   }
 
   /// addFailureResultFile - Add a file to remove if we crash, and returns its
   /// argument.
   const char *addFailureResultFile(const char *Name, const JobAction *JA) {
-    FailureResultFiles[JA] = Name;
+    FailureResultFiles[JA].push_back(Name);
     return Name;
   }
 
@@ -304,8 +304,7 @@ class Compilation {
   /// JobAction.  Otherwise, delete all files in the map.
   /// \param IssueErrors - Report failures as errors.
   /// \return Whether all files were removed successfully.
-  bool CleanupFileMap(const ArgStringMap &Files,
-                      const JobAction *JA,
+  bool CleanupFileMap(const ActionFileMap &Files, const JobAction *JA,
                       bool IssueErrors = false) const;
 
   /// ExecuteCommand - Execute an actual command.
diff --git a/clang/include/clang/Driver/Driver.h 
b/clang/include/clang/Driver/Driver.h
index eece9ac5293f0..8db71b01195d9 100644
--- a/clang/include/clang/Driver/Driver.h
+++ b/clang/include/clang/Driver/Driver.h
@@ -693,11 +693,13 @@ class Driver {
   /// \param BA - The bound architecture.
   /// \param AtTopLevel - Whether this is a "top-level" action.
   /// \param MultipleArchs - Whether multiple -arch options were supplied.
-  /// \param NormalizedTriple - The normalized triple of the relevant target.
+  /// \param OffloadingPrefix - The filename prefix for an offload target.
+  /// \param UseFinalOutput - Whether to use the filename from -o.
   const char *GetNamedOutputPath(Compilation &C, const JobAction &JA,
                                  const char *BaseInput, BoundArch BA,
                                  bool AtTopLevel, bool MultipleArchs,
-                                 StringRef NormalizedTriple) const;
+                                 StringRef OffloadingPrefix,
+                                 bool UseFinalOutput = true) const;
 
   /// GetTemporaryPath - Return the pathname of a temporary file to use
   /// as part of compilation; the file will have the given prefix and suffix.
diff --git a/clang/include/clang/Driver/Job.h b/clang/include/clang/Driver/Job.h
index 03779be5b5a6a..6b104b854c478 100644
--- a/clang/include/clang/Driver/Job.h
+++ b/clang/include/clang/Driver/Job.h
@@ -238,6 +238,13 @@ class Command {
 
   void replaceExecutable(const char *Exe) { Executable = Exe; }
 
+  void replaceOutputFilenames(ArrayRef<InputInfo> Outputs) {
+    OutputFilenames.clear();
+    for (const InputInfo &Output : Outputs)
+      if (Output.isFilename())
+        OutputFilenames.emplace_back(Output.getFilename());
+  }
+
   const char *getExecutable() const { return Executable; }
 
   const llvm::opt::ArgStringList &getArguments() const { return Arguments; }
diff --git a/clang/include/clang/Driver/Util.h 
b/clang/include/clang/Driver/Util.h
index 92d3d40433a32..d57a861833f08 100644
--- a/clang/include/clang/Driver/Util.h
+++ b/clang/include/clang/Driver/Util.h
@@ -21,6 +21,10 @@ namespace driver {
   /// ArgStringMap - Type used to map a JobAction to its result file.
   typedef llvm::DenseMap<const JobAction*, const char*> ArgStringMap;
 
+  /// ActionFileMap - Type used to map a JobAction to its result files.
+  typedef llvm::DenseMap<const JobAction *, SmallVector<const char *, 1>>
+      ActionFileMap;
+
   /// ActionList - Type used for lists of actions.
   typedef SmallVector<Action*, 3> ActionList;
 
diff --git a/clang/lib/Driver/Compilation.cpp b/clang/lib/Driver/Compilation.cpp
index c81c4445a29f9..f3fa01fa9e104 100644
--- a/clang/lib/Driver/Compilation.cpp
+++ b/clang/lib/Driver/Compilation.cpp
@@ -149,16 +149,16 @@ bool Compilation::CleanupFileList(const 
llvm::opt::ArgStringList &Files,
   return Success;
 }
 
-bool Compilation::CleanupFileMap(const ArgStringMap &Files,
-                                 const JobAction *JA,
-                                 bool IssueErrors) const {
+bool Compilation::CleanupFileMap(const ActionFileMap &Files,
+                                 const JobAction *JA, bool IssueErrors) const {
   bool Success = true;
   for (const auto &File : Files) {
     // If specified, only delete the files associated with the JobAction.
     // Otherwise, delete all files in the map.
     if (JA && File.first != JA)
       continue;
-    Success &= CleanupFile(File.second, IssueErrors);
+    for (const char *Filename : File.second)
+      Success &= CleanupFile(Filename, IssueErrors);
   }
   return Success;
 }
diff --git a/clang/lib/Driver/Driver.cpp b/clang/lib/Driver/Driver.cpp
index 38795f7c2ae7a..3813b48000d5e 100644
--- a/clang/lib/Driver/Driver.cpp
+++ b/clang/lib/Driver/Driver.cpp
@@ -5053,6 +5053,11 @@ Driver::BuildOffloadingActions(Compilation &C, 
llvm::opt::DerivedArgList &Args,
       Args.hasFlag(options::OPT_fhip_emit_relocatable,
                    options::OPT_fno_hip_emit_relocatable, false);
 
+  bool HasGPUOutputBundleOption = Args.hasArg(
+      options::OPT_gpu_bundle_output, options::OPT_no_gpu_bundle_output);
+  bool BundleGPUOutput = Args.hasFlag(options::OPT_gpu_bundle_output,
+                                      options::OPT_no_gpu_bundle_output, true);
+
   if (!HIPNoRDC && HIPRelocatableObj)
     C.getDriver().Diag(diag::err_opt_not_valid_with_opt)
         << "-fhip-emit-relocatable"
@@ -5071,6 +5076,7 @@ Driver::BuildOffloadingActions(Compilation &C, 
llvm::opt::DerivedArgList &Args,
 
   ActionList OffloadActions;
   OffloadAction::DeviceDependences DDeps;
+  bool HIPDeviceOnlyNeedsLink = false;
 
   const Action::OffloadKind OffloadKinds[] = {
       Action::OFK_OpenMP, Action::OFK_Cuda, Action::OFK_HIP, Action::OFK_SYCL};
@@ -5159,8 +5165,9 @@ Driver::BuildOffloadingActions(Compilation &C, 
llvm::opt::DerivedArgList &Args,
       }
     }
 
-    // Compiling HIP in device-only non-RDC mode requires linking each action
-    // individually.
+    // Compiling HIP in device-only non-RDC mode requires linking each action.
+    // Keep the inputs unlinked so the linker wrapper can link all device
+    // images in one invocation.
     for (Action *&A : DeviceActions) {
       auto *OffloadTriple = A->getOffloadingToolChain()
                                 ? &A->getOffloadingToolChain()->getTriple()
@@ -5170,12 +5177,19 @@ Driver::BuildOffloadingActions(Compilation &C, 
llvm::opt::DerivedArgList &Args,
           (OffloadTriple->getOS() == llvm::Triple::OSType::AMDHSA ||
            OffloadTriple->getOS() == llvm::Triple::OSType::ChipStar);
 
-      if ((A->getType() != types::TY_Object && !IsHIPSPV &&
-           A->getType() != types::TY_LTO_BC) ||
-          HIPRelocatableObj || !HIPNoRDC || !offloadDeviceOnly())
+      if (HIPRelocatableObj || !HIPNoRDC || !offloadDeviceOnly())
+        continue;
+
+      if (IsHIPSPV) {
+        ActionList LinkerInput = {A};
+        A = C.MakeAction<LinkJobAction>(LinkerInput, types::TY_Image);
+        continue;
+      }
+
+      if (A->getType() != types::TY_Object && A->getType() != types::TY_LTO_BC)
         continue;
-      ActionList LinkerInput = {A};
-      A = C.MakeAction<LinkJobAction>(LinkerInput, types::TY_Image);
+
+      HIPDeviceOnlyNeedsLink = true;
     }
 
     auto *TCAndArch = TCAndArchs.begin();
@@ -5196,18 +5210,18 @@ Driver::BuildOffloadingActions(Compilation &C, 
llvm::opt::DerivedArgList &Args,
     }
   }
 
-  // HIP code in device-only non-RDC mode will bundle the output if it invoked
-  // the linker or if the user explicitly requested it.
+  // HIP code in device-only non-RDC mode will bundle linked output by default
+  // or bundle any output if the user explicitly requested it.
   bool ShouldBundleHIP =
-      Args.hasFlag(options::OPT_gpu_bundle_output,
-                   options::OPT_no_gpu_bundle_output, false) ||
-      (!Args.getLastArg(options::OPT_no_gpu_bundle_output) && HIPNoRDC &&
-       offloadDeviceOnly() && llvm::none_of(OffloadActions, [](Action *A) {
-         return A->getType() != types::TY_Image;
-       }));
+      BundleGPUOutput &&
+      (HasGPUOutputBundleOption ||
+       (HIPNoRDC && offloadDeviceOnly() &&
+        (HIPDeviceOnlyNeedsLink || llvm::none_of(OffloadActions, [](Action *A) 
{
+           return A->getType() != types::TY_Image;
+         }))));
 
   // All kinds exit now in device-only mode except for non-RDC mode HIP.
-  if (offloadDeviceOnly() && !ShouldBundleHIP)
+  if (offloadDeviceOnly() && !ShouldBundleHIP && !HIPDeviceOnlyNeedsLink)
     return C.MakeAction<OffloadAction>(DDeps, types::TY_Nothing);
 
   if (OffloadActions.empty())
@@ -5222,35 +5236,49 @@ Driver::BuildOffloadingActions(Compilation &C, 
llvm::opt::DerivedArgList &Args,
         C.MakeAction<LinkJobAction>(OffloadActions, types::TY_CUDA_FATBIN);
     DDep.add(*FatbinAction, *C.getSingleOffloadToolChain<Action::OFK_Cuda>(),
              /*BA=*/{}, Action::OFK_Cuda);
-  } else if (HIPNoRDC && offloadDeviceOnly()) {
-    // If we are in device-only non-RDC-mode we just emit the final HIP
-    // fatbinary for each translation unit, linking each input individually.
-    Action *FatbinAction =
-        C.MakeAction<LinkJobAction>(OffloadActions, types::TY_HIP_FATBIN);
-    DDep.add(*FatbinAction,
-             *C.getOffloadToolChains<Action::OFK_HIP>().first->second,
-             /*BA=*/{}, Action::OFK_HIP);
   } else if (HIPNoRDC) {
     // Host + device assembly: defer to clang-offload-bundler (see
     // BuildActions).
-    if (HIPAsmBundleDeviceOut &&
+    if (!offloadDeviceOnly() && HIPAsmBundleDeviceOut &&
         shouldBundleHIPAsmWithNewDriver(C, Args, C.getDriver())) {
       for (Action *OA : OffloadActions)
         HIPAsmBundleDeviceOut->push_back(OA);
       return HostAction;
     }
-    // Package all the offloading actions into a single output that can be
-    // embedded in the host and linked.
-    Action *PackagerAction =
-        C.MakeAction<OffloadPackagerJobAction>(OffloadActions, 
types::TY_Image);
 
-    // For HIP non-RDC compilation, wrap the device binary with linker wrapper
-    // before bundling with host code. Do not bind a specific GPU arch here,
-    // as the packaged image may contain entries for multiple GPUs.
-    ActionList AL{PackagerAction};
-    PackagerAction =
-        C.MakeAction<LinkerWrapperJobAction>(AL, types::TY_HIP_FATBIN);
-    DDep.add(*PackagerAction,
+    Action *DeviceOutputAction;
+    if (offloadDeviceOnly() && !HIPDeviceOnlyNeedsLink) {
+      // Non-link outputs use clang-offload-bundler when explicitly requested.
+      DeviceOutputAction =
+          C.MakeAction<LinkJobAction>(OffloadActions, types::TY_HIP_FATBIN);
+    } else {
+      // Package all device inputs so the linker wrapper can link every GPU
+      // architecture in one invocation.
+      Action *Packager = C.MakeAction<OffloadPackagerJobAction>(
+          OffloadActions, types::TY_Image);
+      ActionList WrapperInputs = {Packager};
+      types::ID WrapperOutput = offloadDeviceOnly() && !ShouldBundleHIP
+                                    ? types::TY_Image
+                                    : types::TY_HIP_FATBIN;
+      DeviceOutputAction =
+          C.MakeAction<LinkerWrapperJobAction>(WrapperInputs, WrapperOutput);
+      if (WrapperOutput == types::TY_Image) {
+        auto *WrapperAction = cast<LinkerWrapperJobAction>(DeviceOutputAction);
+        for (Action *A : OffloadActions) {
+          const ToolChain *DeviceTC = nullptr;
+          BoundArch DeviceArch;
+          cast<OffloadAction>(A)->doOnEachDeviceDependence(
+              [&](Action *, const ToolChain *TC, BoundArch BA) {
+                assert(!DeviceTC && "expected one device dependence");
+                DeviceTC = TC;
+                DeviceArch = BA;
+              });
+          assert(DeviceTC && "expected a device toolchain");
+          WrapperAction->registerDeviceOutputInfo(DeviceTC, DeviceArch);
+        }
+      }
+    }
+    DDep.add(*DeviceOutputAction,
              *C.getOffloadToolChains<Action::OFK_HIP>().first->second,
              /*BA=*/{}, Action::OFK_HIP);
   } else {
@@ -5535,6 +5563,12 @@ void Driver::BuildJobs(Compilation &C) const {
   if (FinalOutput) {
     unsigned NumOutputs = 0;
     unsigned NumIfsOutputs = 0;
+    auto CountActionOutputs = [](const Action *A) {
+      const auto *LWA = dyn_cast<LinkerWrapperJobAction>(A);
+      return LWA && !LWA->getDeviceOutputInfo().empty()
+                 ? static_cast<unsigned>(LWA->getDeviceOutputInfo().size())
+                 : 1u;
+    };
     for (const Action *A : C.getActions()) {
       // The actions below do not increase the number of outputs.
       if (A->getKind() == clang::driver::Action::BinaryAnalyzeJobClass ||
@@ -5556,11 +5590,13 @@ void Driver::BuildJobs(Compilation &C) const {
              0 == NumIfsOutputs++) ||
             (A->getKind() == Action::BindArchClass && A->getInputs().size() &&
              A->getInputs().front()->getKind() == Action::IfsMergeJobClass)))
-        ++NumOutputs;
+        NumOutputs += CountActionOutputs(A);
       else if (A->getKind() == Action::OffloadClass &&
                A->getType() == types::TY_Nothing &&
-               !C.getArgs().hasArg(options::OPT_fsyntax_only))
-        NumOutputs += A->size();
+               !C.getArgs().hasArg(options::OPT_fsyntax_only)) {
+        for (const Action *Input : A->getInputs())
+          NumOutputs += CountActionOutputs(Input);
+      }
     }
 
     if (NumOutputs > 1) {
@@ -6336,7 +6372,7 @@ InputInfoList Driver::BuildJobsForActionNoCache(
 
   // Determine the place to write output to, if any.
   InputInfo Result;
-  InputInfoList UnbundlingResults;
+  InputInfoList MultipleResults;
   if (auto *UA = dyn_cast<OffloadUnbundlingJobAction>(JA)) {
     // If we have an unbundling job, we need to create results for all the
     // outputs. We also update the results cache so that other actions using
@@ -6361,7 +6397,7 @@ InputInfoList Driver::BuildJobsForActionNoCache(
                              OffloadingPrefix),
           BaseInput);
       // Save the unbundling result.
-      UnbundlingResults.push_back(CurI);
+      MultipleResults.push_back(CurI);
 
       // Get the unique string identifier for this dependence and cache the
       // result.
@@ -6386,6 +6422,23 @@ InputInfoList Driver::BuildJobsForActionNoCache(
     assert(CachedResults.find(ActionTC) != CachedResults.end() &&
            "Result does not exist??");
     Result = CachedResults[ActionTC].front();
+  } else if (auto *LWA = dyn_cast<LinkerWrapperJobAction>(JA);
+             LWA && !LWA->getDeviceOutputInfo().empty()) {
+    ArrayRef<LinkerWrapperJobAction::DeviceOutputInfo> OutputInfo =
+        LWA->getDeviceOutputInfo();
+    for (const auto &Info : OutputInfo) {
+      std::string OffloadingPrefix = Action::GetOffloadingFileNamePrefix(
+          Action::OFK_HIP, Info.DeviceToolChain->getTripleString(),
+          /*CreatePrefixForHost=*/false);
+      MultipleResults.emplace_back(
+          LWA,
+          GetNamedOutputPath(C, *LWA, BaseInput, Info.DeviceBoundArch,
+                             /*AtTopLevel=*/true, /*MultipleArchs=*/true,
+                             OffloadingPrefix,
+                             /*UseFinalOutput=*/OutputInfo.size() == 1),
+          BaseInput);
+    }
+    Result = MultipleResults.front();
   } else if (JA->getType() == types::TY_Nothing)
     Result = {InputInfo(A, BaseInput)};
   else {
@@ -6412,24 +6465,26 @@ InputInfoList Driver::BuildJobsForActionNoCache(
       if (i + 1 != e)
         llvm::errs() << ", ";
     }
-    if (UnbundlingResults.empty())
+    if (MultipleResults.empty())
       llvm::errs() << "], output: " << Result.getAsString() << "\n";
     else {
       llvm::errs() << "], outputs: [";
-      for (unsigned i = 0, e = UnbundlingResults.size(); i != e; ++i) {
-        llvm::errs() << UnbundlingResults[i].getAsString();
+      for (unsigned i = 0, e = MultipleResults.size(); i != e; ++i) {
+        llvm::errs() << MultipleResults[i].getAsString();
         if (i + 1 != e)
           llvm::errs() << ", ";
       }
       llvm::errs() << "] \n";
     }
   } else {
-    if (UnbundlingResults.empty())
+    if (MultipleResults.empty())
       T->ConstructJob(C, *JA, Result, InputInfos, Args, LinkingOutput);
     else
-      T->ConstructJobMultipleOutputs(C, *JA, UnbundlingResults, InputInfos,
-                                     Args, LinkingOutput);
+      T->ConstructJobMultipleOutputs(C, *JA, MultipleResults, InputInfos, Args,
+                                     LinkingOutput);
   }
+  if (isa<LinkerWrapperJobAction>(JA) && !MultipleResults.empty())
+    return MultipleResults;
   return {Result};
 }
 
@@ -6547,7 +6602,8 @@ static const char *GetModuleOutputPath(Compilation &C, 
const JobAction &JA,
 const char *Driver::GetNamedOutputPath(Compilation &C, const JobAction &JA,
                                        const char *BaseInput, BoundArch BA,
                                        bool AtTopLevel, bool MultipleArchs,
-                                       StringRef OffloadingPrefix) const {
+                                       StringRef OffloadingPrefix,
+                                       bool UseFinalOutput) const {
   std::string BoundArchStr = sanitizeTargetIDInFileName(BA.ArchName);
 
   llvm::PrettyStackTraceString CrashInfo("Computing output path");
@@ -6578,7 +6634,8 @@ const char *Driver::GetNamedOutputPath(Compilation &C, 
const JobAction &JA,
 
   // Output to a user requested destination?
   if (AtTopLevel && !isa<DsymutilJobAction>(JA) && !isa<VerifyJobAction>(JA)) {
-    if (Arg *FinalOutput = C.getArgs().getLastArg(options::OPT_o))
+    if (Arg *FinalOutput =
+            UseFinalOutput ? C.getArgs().getLastArg(options::OPT_o) : nullptr)
       return C.addResultFile(FinalOutput->getValue(), &JA);
   }
 
diff --git a/clang/lib/Driver/ToolChains/AMDGPU.cpp 
b/clang/lib/Driver/ToolChains/AMDGPU.cpp
index 5893f6f6b2915..b5ce9e0fda9a2 100644
--- a/clang/lib/Driver/ToolChains/AMDGPU.cpp
+++ b/clang/lib/Driver/ToolChains/AMDGPU.cpp
@@ -1282,6 +1282,7 @@ LTOKind AMDGPUToolChain::getLTOMode(const ArgList &Args,
                                     Action::OffloadKind Kind) const {
   if (getTriple().isAMDGCN() && getDriver().offloadDeviceOnly() &&
       !Args.hasFlag(options::OPT_fgpu_rdc, options::OPT_fno_gpu_rdc, false) &&
+      !ToolChain::needsProfileRT(Args) &&
       !Args.hasArg(options::OPT_foffload_lto, options::OPT_foffload_lto_EQ))
     return LTOK_None;
   return ToolChain::getLTOMode(Args, Kind);
diff --git a/clang/lib/Driver/ToolChains/Clang.cpp 
b/clang/lib/Driver/ToolChains/Clang.cpp
index 94f9a26aac39f..d1bddc840dd55 100644
--- a/clang/lib/Driver/ToolChains/Clang.cpp
+++ b/clang/lib/Driver/ToolChains/Clang.cpp
@@ -9750,8 +9750,13 @@ static bool requiresProfileRT(unsigned ID) {
   switch (ID) {
   case options::OPT_fprofile_generate:
   case options::OPT_fprofile_generate_EQ:
+  case options::OPT_fcs_profile_generate:
+  case options::OPT_fcs_profile_generate_EQ:
   case options::OPT_fprofile_instr_generate:
   case options::OPT_fprofile_instr_generate_EQ:
+  case options::OPT_fcreate_profile:
+  case options::OPT_fprofile_generate_cold_function_coverage:
+  case options::OPT_fprofile_generate_cold_function_coverage_EQ:
   case options::OPT_fcoverage_mapping:
   case options::OPT_fno_coverage_mapping:
   case options::OPT_fcoverage_compilation_dir_EQ:
@@ -9781,7 +9786,24 @@ void LinkerWrapper::ConstructJob(Compilation &C, const 
JobAction &JA,
                                  const InputInfoList &Inputs,
                                  const ArgList &Args,
                                  const char *LinkingOutput) const {
+  constructJob(C, JA, InputInfoList{Output}, Inputs, Args, LinkingOutput);
+}
+
+void LinkerWrapper::ConstructJobMultipleOutputs(
+    Compilation &C, const JobAction &JA, const InputInfoList &Outputs,
+    const InputInfoList &Inputs, const ArgList &Args,
+    const char *LinkingOutput) const {
+  constructJob(C, JA, Outputs, Inputs, Args, LinkingOutput);
+}
+
+void LinkerWrapper::constructJob(Compilation &C, const JobAction &JA,
+                                 const InputInfoList &Outputs,
+                                 const InputInfoList &Inputs,
+                                 const ArgList &Args,
+                                 const char *LinkingOutput) const {
   using namespace options;
+  assert(!Outputs.empty() && "expected at least one output");
+  const InputInfo &Output = Outputs.front();
 
   // A list of permitted options that will be forwarded to the embedded device
   // compilation job.
@@ -9822,8 +9844,16 @@ void LinkerWrapper::ConstructJob(Compilation &C, const 
JobAction &JA,
       OPT_fmultilib_flag,
       OPT_fprofile_generate,
       OPT_fprofile_generate_EQ,
+      OPT_fcs_profile_generate,
+      OPT_fcs_profile_generate_EQ,
       OPT_fprofile_instr_generate,
       OPT_fprofile_instr_generate_EQ,
+      OPT_fcreate_profile,
+      OPT_fprofile_generate_cold_function_coverage,
+      OPT_fprofile_generate_cold_function_coverage_EQ,
+      OPT_fno_profile_generate,
+      OPT_fno_profile_instr_generate,
+      OPT_noprofilelib,
       OPT_fcoverage_mapping,
       OPT_fno_coverage_mapping,
       OPT_fcoverage_compilation_dir_EQ,
@@ -9848,13 +9878,15 @@ void LinkerWrapper::ConstructJob(Compilation &C, const 
JobAction &JA,
     return TC.getVFS().exists(
         TC.getCompilerRT(Args, Name, ToolChain::FT_Static));
   };
-  auto ShouldForwardForToolChain = [&](Arg *A, const ToolChain &TC) {
+  auto ShouldForwardForToolChain = [&](Arg *A, const ToolChain &TC,
+                                       const ArgList &TCArgs) {
     unsigned ID = A->getOption().getID();
     // Don't forward profiling arguments if the toolchain doesn't support it.
     // Without this check using it on the host would result in linker errors.
     // Coverage mapping flags require -fprofile-instr-generate, so drop them
     // together to avoid a device cc1 diagnostic.
-    if (requiresProfileRT(ID) && !ToolChainHasRT(TC, "profile"))
+    if (requiresProfileRT(ID) && !TCArgs.hasArg(OPT_noprofilelib) &&
+        !ToolChainHasRT(TC, "profile"))
       return false;
     // Don't forward sanitizer arguments if the toolchain doesn't support it.
     // Without this check using it on the host would result in linker errors.
@@ -9864,13 +9896,13 @@ void LinkerWrapper::ConstructJob(Compilation &C, const 
JobAction &JA,
     return TC.HasNativeLLVMSupport() || ID != OPT_mllvm;
   };
   auto ShouldForward = [&](const llvm::DenseSet<unsigned> &Set, Arg *A,
-                           const ToolChain &TC) {
+                           const ToolChain &TC, const ArgList &TCArgs) {
     if (A->getOption().matches(OPT_v) && SuppressHIPNoRDCVerbose)
       return false;
     return (Set.contains(A->getOption().getID()) ||
             (A->getOption().getGroup().isValid() &&
              Set.contains(A->getOption().getGroup().getID()))) &&
-           ShouldForwardForToolChain(A, TC);
+           ShouldForwardForToolChain(A, TC, TCArgs);
   };
 
   ArgStringList CmdArgs;
@@ -9889,10 +9921,10 @@ void LinkerWrapper::ConstructJob(Compilation &C, const 
JobAction &JA,
       for (Arg *A : ToolChainArgs) {
         if (A->getOption().matches(OPT_Zlinker_input))
           LinkerArgs.emplace_back(A->getValue());
-        else if (ShouldForward(CompilerOptions, A, *TC)) {
+        else if (ShouldForward(CompilerOptions, A, *TC, ToolChainArgs)) {
           A->claim();
           A->render(Args, CompilerArgs);
-        } else if (ShouldForward(LinkerOptions, A, *TC)) {
+        } else if (ShouldForward(LinkerOptions, A, *TC, ToolChainArgs)) {
           A->claim();
           A->render(Args, LinkerArgs);
         }
@@ -9999,6 +10031,7 @@ void LinkerWrapper::ConstructJob(Compilation &C, const 
JobAction &JA,
   // Construct the link job so we can wrap around it.
   Linker->ConstructJob(C, JA, Output, Inputs, Args, LinkingOutput);
   const auto &LinkCommand = C.getJobs().getJobs().back();
+  LinkCommand->replaceOutputFilenames(Outputs);
 
   // Forward -Xoffload-{compiler,linker}<-triple> arguments to the linker
   // wrapper.
@@ -10070,11 +10103,39 @@ void LinkerWrapper::ConstructJob(Compilation &C, 
const JobAction &JA,
                                        LinkCommand->getExecutable()));
 
   // We use action type to differentiate two use cases of the linker wrapper.
-  // TY_Image for normal linker wrapper work.
+  // TY_Image for normal linker wrapper work or unbundled device images.
   // TY_HIP_FATBIN for HIP device-only links emitting a fat binary directly.
   assert(JA.getType() == types::TY_HIP_FATBIN ||
          JA.getType() == types::TY_Image);
-  if (JA.getType() == types::TY_HIP_FATBIN) {
+  bool EmitDeviceImagesOnly = JA.getType() == types::TY_Image &&
+                              JA.isDeviceOffloading(Action::OFK_HIP) &&
+                              C.getDriver().offloadDeviceOnly();
+  if (EmitDeviceImagesOnly) {
+    CmdArgs.push_back("--emit-device-images-only");
+    if (Outputs.size() == 1 && Args.hasArg(OPT_o)) {
+      CmdArgs.append({"-o", Output.getFilename()});
+    } else {
+      auto OutputInfo = cast<LinkerWrapperJobAction>(JA).getDeviceOutputInfo();
+      assert(OutputInfo.size() == Outputs.size() &&
+             "device output metadata does not match outputs");
+      for (auto [Info, DeviceOutput] : llvm::zip_equal(OutputInfo, Outputs)) {
+        const ArgList &DeviceArgs = C.getArgsForToolChain(
+            Info.DeviceToolChain, Info.DeviceBoundArch, Action::OFK_HIP);
+        BoundArch Arch = Info.DeviceBoundArch;
+        if (Arch.empty())
+          Arch = BoundArch(DeviceArgs.getLastArgValue(OPT_march_EQ));
+        std::string Triple =
+            Info.DeviceToolChain->ComputeEffectiveClangTriple(DeviceArgs, 
Arch);
+        StringRef ArchName =
+            Arch.empty() ? StringRef("generic") : Arch.ArchName;
+        CmdArgs.push_back(Args.MakeArgString(Twine("--device-image-output=") +
+                                             Triple + "," + ArchName));
+        CmdArgs.push_back(DeviceOutput.getFilename());
+      }
+    }
+    for (auto Input : Inputs)
+      CmdArgs.push_back(Input.getFilename());
+  } else if (JA.getType() == types::TY_HIP_FATBIN) {
     CmdArgs.push_back("--emit-fatbin-only");
     CmdArgs.append({"-o", Output.getFilename()});
     for (auto Input : Inputs)
diff --git a/clang/lib/Driver/ToolChains/Clang.h 
b/clang/lib/Driver/ToolChains/Clang.h
index 23f270ffb76a3..6860cfe574135 100644
--- a/clang/lib/Driver/ToolChains/Clang.h
+++ b/clang/lib/Driver/ToolChains/Clang.h
@@ -178,6 +178,11 @@ class LLVM_LIBRARY_VISIBILITY OffloadPackager final : 
public Tool {
 class LLVM_LIBRARY_VISIBILITY LinkerWrapper final : public Tool {
   const Tool *Linker;
 
+  void constructJob(Compilation &C, const JobAction &JA,
+                    const InputInfoList &Outputs, const InputInfoList &Inputs,
+                    const llvm::opt::ArgList &TCArgs,
+                    const char *LinkingOutput) const;
+
 public:
   LinkerWrapper(const ToolChain &TC, const Tool *Linker)
       : Tool("Offload::Linker", "linker", TC), Linker(Linker) {}
@@ -187,6 +192,11 @@ class LLVM_LIBRARY_VISIBILITY LinkerWrapper final : public 
Tool {
                     const InputInfo &Output, const InputInfoList &Inputs,
                     const llvm::opt::ArgList &TCArgs,
                     const char *LinkingOutput) const override;
+  void ConstructJobMultipleOutputs(Compilation &C, const JobAction &JA,
+                                   const InputInfoList &Outputs,
+                                   const InputInfoList &Inputs,
+                                   const llvm::opt::ArgList &TCArgs,
+                                   const char *LinkingOutput) const override;
 };
 
 // Calculate the output path of the module file when compiling a module unit
diff --git a/clang/test/Driver/hip-binding.hip 
b/clang/test/Driver/hip-binding.hip
index b0839021aa1d9..cc98c3f2d0026 100644
--- a/clang/test/Driver/hip-binding.hip
+++ b/clang/test/Driver/hip-binding.hip
@@ -57,24 +57,43 @@
 // RUN:        --offload-arch=gfx90a --offload-arch=gfx908 
--offload-device-only -c %s 2>&1 \
 // RUN: | FileCheck -check-prefix=MULTI-D-ONLY %s
 //      MULTI-D-ONLY: # "amdgcn-amd-amdhsa" - "clang", inputs: 
["[[INPUT:.+]]"], output: "[[GFX908:.+]]"
-// MULTI-D-ONLY-NEXT: # "amdgcn-amd-amdhsa" - "AMDGCN::Linker", inputs: 
["[[GFX908]]"], output: "[[GFX908_OUT:.+]]"
 // MULTI-D-ONLY-NEXT: # "amdgcn-amd-amdhsa" - "clang", inputs: ["[[INPUT]]"], 
output: "[[GFX90a:.+]]"
-// MULTI-D-ONLY-NEXT: # "amdgcn-amd-amdhsa" - "AMDGCN::Linker", inputs: 
["[[GFX90a]]"], output: "[[GFX90a_OUT:.+]]"
-// MULTI-D-ONLY-NEXT: # "amdgcn-amd-amdhsa" - "AMDGCN::Linker", inputs: 
["[[GFX908_OUT]]", "[[GFX90a_OUT]]"], output: "{{.+}}"
+// MULTI-D-ONLY-NEXT: # "amdgcn-amd-amdhsa" - "Offload::Packager", inputs: 
["[[GFX908]]", "[[GFX90a]]"], output: "[[PACKAGE:.+]]"
+// MULTI-D-ONLY-NEXT: # "amdgcn-amd-amdhsa" - "Offload::Linker", inputs: 
["[[PACKAGE]]"], output: "{{.+}}"
 //
-// RUN: not %clang -### --target=x86_64-linux-gnu --offload-new-driver 
-ccc-print-bindings -nogpulib -nogpuinc -emit-llvm \
+// RUN: %clang -### --target=x86_64-linux-gnu --offload-new-driver 
-ccc-print-bindings -nogpulib -nogpuinc \
+// RUN:        --no-gpu-bundle-output --gpu-bundle-output 
--offload-arch=gfx90a --offload-arch=gfx908 --offload-device-only -c %s 2>&1 \
+// RUN: | FileCheck -check-prefix=MULTI-D-ONLY %s
+//
+// RUN: %clang -### --target=x86_64-linux-gnu --offload-new-driver 
-ccc-print-bindings -nogpulib -nogpuinc \
+// RUN:        --no-gpu-bundle-output --offload-arch=gfx90a 
--offload-arch=gfx908 --offload-device-only -c %s 2>&1 \
+// RUN: | FileCheck -check-prefix=MULTI-D-ONLY-NO-BUNDLE %s
+//      MULTI-D-ONLY-NO-BUNDLE: # "amdgcn-amd-amdhsa" - "clang", inputs: 
["[[INPUT:.+]]"], output: "[[GFX908:.+]]"
+// MULTI-D-ONLY-NO-BUNDLE-NEXT: # "amdgcn-amd-amdhsa" - "clang", inputs: 
["[[INPUT]]"], output: "[[GFX90A:.+]]"
+// MULTI-D-ONLY-NO-BUNDLE-NEXT: # "amdgcn-amd-amdhsa" - "Offload::Packager", 
inputs: ["[[GFX908]]", "[[GFX90A]]"], output: "[[PACKAGE:.+]]"
+// MULTI-D-ONLY-NO-BUNDLE-NEXT: # "amdgcn-amd-amdhsa" - "Offload::Linker", 
inputs: ["[[PACKAGE]]"], outputs: 
["{{.*}}hip-binding-hip-amdgcn-amd-amdhsa-gfx908.out", 
"{{.*}}hip-binding-hip-amdgcn-amd-amdhsa-gfx90a.out"]
+//
+// RUN: %clang -### --target=x86_64-linux-gnu --offload-new-driver 
-ccc-print-bindings -nogpulib -nogpuinc \
+// RUN:        --gpu-bundle-output --no-gpu-bundle-output 
--offload-arch=gfx90a --offload-arch=gfx908 --offload-device-only -c %s 2>&1 \
+// RUN: | FileCheck -check-prefix=MULTI-D-ONLY-NO-BUNDLE %s
+//
+// RUN: not %clang -### --target=x86_64-linux-gnu --offload-new-driver 
-ccc-print-bindings -nogpulib -nogpuinc \
 // RUN:        --no-gpu-bundle-output --offload-arch=gfx90a 
--offload-arch=gfx908 --offload-device-only -c -o %t %s 2>&1 \
 // RUN: | FileCheck -check-prefix=MULTI-D-ONLY-NO-BUNDLE-O %s
 // MULTI-D-ONLY-NO-BUNDLE-O: error: cannot specify -o when generating multiple 
output files
 
+// RUN: %clang -### --target=x86_64-linux-gnu -ccc-print-bindings -nogpulib 
-nogpuinc \
+// RUN:        --no-gpu-bundle-output --offload-arch=gfx90a 
--offload-device-only -c %s 2>&1 \
+// RUN: | FileCheck -check-prefix=SINGLE-D-ONLY-NO-BUNDLE %s
+// SINGLE-D-ONLY-NO-BUNDLE: # "amdgcn-amd-amdhsa" - "Offload::Linker", inputs: 
["{{.+}}"], outputs: ["{{.*}}hip-binding-hip-amdgcn-amd-amdhsa-gfx90a.out"]
+
 // RUN: %clang -### --target=x86_64-linux-gnu --offload-new-driver 
-ccc-print-bindings -nogpulib -nogpuinc \
 // RUN:        --gpu-bundle-output --offload-arch=gfx90a --offload-arch=gfx908 
--offload-device-only -c -o a.out %s 2>&1 \
 // RUN: | FileCheck -check-prefix=MULTI-D-ONLY-O %s
 //      MULTI-D-ONLY-O: "amdgcn-amd-amdhsa" - "clang", inputs: 
["[[INPUT:.+]]"], output: "[[GFX908_OBJ:.+]]"
-// MULTI-D-ONLY-O-NEXT: "amdgcn-amd-amdhsa" - "AMDGCN::Linker", inputs: 
["[[GFX908_OBJ]]"], output: "[[GFX908:.+]]"
 // MULTI-D-ONLY-O-NEXT: "amdgcn-amd-amdhsa" - "clang", inputs: ["[[INPUT]]"], 
output: "[[GFX90A_OBJ:.+]]"
-// MULTI-D-ONLY-O-NEXT: "amdgcn-amd-amdhsa" - "AMDGCN::Linker", inputs: 
["[[GFX90A_OBJ]]"], output: "[[GFX90A:.+]]"
-// MULTI-D-ONLY-O-NEXT: "amdgcn-amd-amdhsa" - "AMDGCN::Linker", inputs: 
["[[GFX908]]", "[[GFX90A]]"], output: "a.out"
+// MULTI-D-ONLY-O-NEXT: "amdgcn-amd-amdhsa" - "Offload::Packager", inputs: 
["[[GFX908_OBJ]]", "[[GFX90A_OBJ]]"], output: "[[PACKAGE:.+]]"
+// MULTI-D-ONLY-O-NEXT: "amdgcn-amd-amdhsa" - "Offload::Linker", inputs: 
["[[PACKAGE]]"], output: "a.out"
 
 // RUN: %clang -### --target=x86_64-linux-gnu --offload-new-driver 
-ccc-print-bindings -nogpulib -nogpuinc -emit-llvm \
 // RUN:        --gpu-bundle-output --offload-arch=gfx90a --offload-arch=gfx908 
--offload-device-only -c -o a.out %s 2>&1 \
diff --git a/clang/test/Driver/hip-phases.hip b/clang/test/Driver/hip-phases.hip
index 8fb58dc98cefc..9165a566c8366 100644
--- a/clang/test/Driver/hip-phases.hip
+++ b/clang/test/Driver/hip-phases.hip
@@ -701,13 +701,13 @@
 // SPIRV-ONLY-NEXT: 2: compiler, {1}, ir, (device-hip, gfx1030)
 // SPIRV-ONLY-NEXT: 3: backend, {2}, assembler, (device-hip, gfx1030)
 // SPIRV-ONLY-NEXT: 4: assembler, {3}, object, (device-hip, gfx1030)
-// SPIRV-ONLY-NEXT: 5: linker, {4}, image, (device-hip, gfx1030)
-// SPIRV-ONLY-NEXT: 6: offload, "device-hip (amdgcn-amd-amdhsa:gfx1030)" {5}, 
image
-// SPIRV-ONLY-NEXT: 7: input, "[[INPUT]]", hip, (device-hip, amdgcnspirv)
-// SPIRV-ONLY-NEXT: 8: preprocessor, {7}, hip-cpp-output, (device-hip, 
amdgcnspirv)
-// SPIRV-ONLY-NEXT: 9: compiler, {8}, ir, (device-hip, amdgcnspirv)
-// SPIRV-ONLY-NEXT: 10: backend, {9}, lto-bc, (device-hip, amdgcnspirv)
-// SPIRV-ONLY-NEXT: 11: linker, {10}, image, (device-hip, amdgcnspirv)
-// SPIRV-ONLY-NEXT: 12: offload, "device-hip (spirv64-amd-amdhsa:amdgcnspirv)" 
{11}, image
-// SPIRV-ONLY-NEXT: 13: linker, {6, 12}, hip-fatbin, (device-hip)
+// SPIRV-ONLY-NEXT: 5: offload, "device-hip (amdgcn-amd-amdhsa:gfx1030)" {4}, 
object
+// SPIRV-ONLY-NEXT: 6: input, "[[INPUT]]", hip, (device-hip, amdgcnspirv)
+// SPIRV-ONLY-NEXT: 7: preprocessor, {6}, hip-cpp-output, (device-hip, 
amdgcnspirv)
+// SPIRV-ONLY-NEXT: 8: compiler, {7}, ir, (device-hip, amdgcnspirv)
+// SPIRV-ONLY-NEXT: 9: backend, {8}, lto-bc, (device-hip, amdgcnspirv)
+// SPIRV-ONLY-NEXT: 10: linker, {9}, image, (device-hip, amdgcnspirv)
+// SPIRV-ONLY-NEXT: 11: offload, "device-hip (spirv64-amd-amdhsa:amdgcnspirv)" 
{10}, image
+// SPIRV-ONLY-NEXT: 12: llvm-offload-binary, {5, 11}, image, (device-hip)
+// SPIRV-ONLY-NEXT: 13: clang-linker-wrapper, {12}, hip-fatbin, (device-hip)
 // SPIRV-ONLY-NEXT: 14: offload, "device-hip (amdgcn-amd-amdhsa)" {13}, none
diff --git a/clang/test/Driver/hip-profile-rocm-runtime.hip 
b/clang/test/Driver/hip-profile-rocm-runtime.hip
index fc82db4fc13c0..2a9ed0eee78ba 100644
--- a/clang/test/Driver/hip-profile-rocm-runtime.hip
+++ b/clang/test/Driver/hip-profile-rocm-runtime.hip
@@ -3,9 +3,10 @@
 
 // Build a fake resource dir containing both the base profile runtime and the
 // ROCm device-profile runtime so the driver's existence check passes.
-// RUN: rm -rf %t && mkdir -p %t/lib/x86_64-unknown-linux
+// RUN: rm -rf %t && mkdir -p %t/lib/x86_64-unknown-linux 
%t/lib/amdgcn-amd-amdhsa
 // RUN: touch %t/lib/x86_64-unknown-linux/libclang_rt.profile.a
 // RUN: touch %t/lib/x86_64-unknown-linux/libclang_rt.profile_rocm.a
+// RUN: touch %t/lib/amdgcn-amd-amdhsa/libclang_rt.profile.a
 // RUN: touch %t.o
 
 // HIP host link with PGO links clang_rt.profile_rocm.
@@ -31,3 +32,60 @@
 // RUN:   | FileCheck -check-prefix=HOST-PGO %s
 // HOST-PGO: "{{.*}}libclang_rt.profile.a"
 // HOST-PGO-NOT: libclang_rt.profile_rocm.a
+
+// HIP device-only code object builds use the linker wrapper so its inner Clang
+// invocation links the AMDGPU profile runtime.
+// RUN: %clang -### -x hip --cuda-device-only \
+// RUN:   --target=x86_64-unknown-linux --offload-arch=gfx906 \
+// RUN:   -fprofile-generate -resource-dir=%t -nogpuinc -nogpulib %s 2>&1 \
+// RUN:   | FileCheck -check-prefix=HIP-DEVICE-PGO %s
+// HIP-DEVICE-PGO-DAG: "-emit-llvm-bc"
+// HIP-DEVICE-PGO-DAG: "-flto=full"
+// HIP-DEVICE-PGO: "{{.*}}llvm-offload-binary"
+// HIP-DEVICE-PGO: "{{.*}}clang-linker-wrapper"
+// HIP-DEVICE-PGO-SAME: 
"--device-compiler=amdgcn-amd-amdhsa=-fprofile-generate"
+// HIP-DEVICE-PGO-SAME: "--emit-fatbin-only"
+
+// Unbundled code object output uses the linker wrapper's raw image mode.
+// RUN: %clang -### -x hip --cuda-device-only --no-gpu-bundle-output \
+// RUN:   --target=x86_64-unknown-linux --offload-arch=gfx906 \
+// RUN:   -fprofile-generate -resource-dir=%t -nogpuinc -nogpulib %s 2>&1 \
+// RUN:   | FileCheck -check-prefix=HIP-DEVICE-PGO-RAW %s
+// HIP-DEVICE-PGO-RAW: "{{.*}}llvm-offload-binary"
+// HIP-DEVICE-PGO-RAW: "{{.*}}clang-linker-wrapper"
+// HIP-DEVICE-PGO-RAW-SAME: 
"--device-compiler=amdgcn-amd-amdhsa=-fprofile-generate"
+// HIP-DEVICE-PGO-RAW-SAME: "--emit-device-images-only"
+
+// Other profile generation modes are also forwarded to the linker wrapper.
+// RUN: %clang -### -x hip --cuda-device-only \
+// RUN:   --target=x86_64-unknown-linux --offload-arch=gfx906 \
+// RUN:   -fcs-profile-generate -resource-dir=%t -nogpuinc -nogpulib %s 2>&1 \
+// RUN:   | FileCheck -check-prefix=HIP-DEVICE-CSPGO %s
+// HIP-DEVICE-CSPGO: "{{.*}}clang-linker-wrapper"
+// HIP-DEVICE-CSPGO-SAME: 
"--device-compiler=amdgcn-amd-amdhsa=-fcs-profile-generate"
+
+// RUN: %clang -### -x hip --cuda-device-only \
+// RUN:   --target=x86_64-unknown-linux --offload-arch=gfx906 \
+// RUN:   -fcreate-profile -resource-dir=%t -nogpuinc -nogpulib %s 2>&1 \
+// RUN:   | FileCheck -check-prefix=HIP-DEVICE-CREATE-PROFILE %s
+// HIP-DEVICE-CREATE-PROFILE: "{{.*}}clang-linker-wrapper"
+// HIP-DEVICE-CREATE-PROFILE-SAME: 
"--device-compiler=amdgcn-amd-amdhsa=-fcreate-profile"
+
+// RUN: %clang -### -x hip --cuda-device-only \
+// RUN:   --target=x86_64-unknown-linux --offload-arch=gfx906 \
+// RUN:   -fprofile-generate-cold-function-coverage -resource-dir=%t \
+// RUN:   -nogpuinc -nogpulib %s 2>&1 \
+// RUN:   | FileCheck -check-prefix=HIP-DEVICE-COLD-PROFILE %s
+// HIP-DEVICE-COLD-PROFILE: "{{.*}}clang-linker-wrapper"
+// HIP-DEVICE-COLD-PROFILE-SAME: 
"--device-compiler=amdgcn-amd-amdhsa=-fprofile-generate-cold-function-coverage"
+
+// -noprofilelib must reach the inner Clang together with the profile option.
+// RUN: rm %t/lib/amdgcn-amd-amdhsa/libclang_rt.profile.a
+// RUN: %clang -### -x hip --cuda-device-only \
+// RUN:   --target=x86_64-unknown-linux --offload-arch=gfx906 \
+// RUN:   -fprofile-generate -noprofilelib -resource-dir=%t \
+// RUN:   -nogpuinc -nogpulib %s 2>&1 \
+// RUN:   | FileCheck -check-prefix=HIP-DEVICE-NOPROFILELIB %s
+// HIP-DEVICE-NOPROFILELIB: "{{.*}}clang-linker-wrapper"
+// HIP-DEVICE-NOPROFILELIB-SAME: 
"--device-compiler=amdgcn-amd-amdhsa=-fprofile-generate"
+// HIP-DEVICE-NOPROFILELIB-SAME: 
"--device-compiler=amdgcn-amd-amdhsa=-noprofilelib"
diff --git a/clang/test/Driver/hip-toolchain-device-only.hip 
b/clang/test/Driver/hip-toolchain-device-only.hip
index c0621854f17ce..279784e5305e5 100644
--- a/clang/test/Driver/hip-toolchain-device-only.hip
+++ b/clang/test/Driver/hip-toolchain-device-only.hip
@@ -6,18 +6,22 @@
 // CHECK-NOT: error:
 
 // CHECK: [[CLANG:".*clang.*"]] "-cc1"{{.*}} "-triple" "amdgcn-amd-amdhsa"
+// CHECK-SAME: "-emit-obj"
 // CHECK-SAME: "-fcuda-is-device"
 // CHECK-SAME: "-target-cpu" "gfx803"
-// CHECK-SAME: {{.*}} "-o" [[OBJ_DEV_A_803:".*o"]] "-x" "hip"
-
-// CHECK: [[LLD: ".*lld.*"]] "-flavor" "gnu" "-m" "elf64_amdgpu" 
"--no-undefined" "-shared"
-// CHECK-SAME: "-o" "[[IMG_DEV_A_803:.*out]]" [[OBJ_DEV_A_803]]
+// CHECK-SAME: {{.*}} "-o" "[[OBJ_DEV_A_803:[^"]+\.o]]" "-x" "hip"
 
 // CHECK: [[CLANG:".*clang.*"]] "-cc1"{{.*}} "-triple" "amdgcn-amd-amdhsa"
 // CHECK-SAME: "-emit-obj"
 // CHECK-SAME: "-fcuda-is-device"
 // CHECK-SAME: "-target-cpu" "gfx900"
-// CHECK-SAME: {{.*}} "-o" [[OBJ_DEV_A_900:".*o"]] "-x" "hip"
+// CHECK-SAME: {{.*}} "-o" "[[OBJ_DEV_A_900:[^"]+\.o]]" "-x" "hip"
+
+// CHECK: [[PACKAGER:"[^"]*llvm-offload-binary[^"]*"]] "-o" "[[PACKAGE:[^"]+]]"
+// CHECK-SAME: 
"--image=file=[[OBJ_DEV_A_803]],triple=amdgcn-amd-amdhsa,arch=gfx803,kind=hip"
+// CHECK-SAME: 
"--image=file=[[OBJ_DEV_A_900]],triple=amdgcn-amd-amdhsa,arch=gfx900,kind=hip"
 
-// CHECK: [[LLD]] "-flavor" "gnu" "-m" "elf64_amdgpu" "--no-undefined" 
"-shared"
-// CHECK-SAME: "-o" "[[IMG_DEV_A_900:.*out]]" [[OBJ_DEV_A_900]]
+// CHECK: [[WRAPPER:"[^"]*clang-linker-wrapper[^"]*"]]
+// CHECK-SAME: "--should-extract=gfx803" "--should-extract=gfx900"
+// CHECK-SAME: "--host-triple=x86_64-unknown-linux-gnu"
+// CHECK-SAME: "--emit-fatbin-only" "-o" "{{[^"]+}}" "[[PACKAGE]]"
diff --git 
a/clang/test/OffloadTools/clang-linker-wrapper/linker-wrapper-hip-no-rdc.c 
b/clang/test/OffloadTools/clang-linker-wrapper/linker-wrapper-hip-no-rdc.c
index 44d0c1e2b7b51..a313b3011563f 100644
--- a/clang/test/OffloadTools/clang-linker-wrapper/linker-wrapper-hip-no-rdc.c
+++ b/clang/test/OffloadTools/clang-linker-wrapper/linker-wrapper-hip-no-rdc.c
@@ -12,6 +12,8 @@ __attribute__((visibility("protected"), used)) int x;
 // RUN: llvm-offload-binary -o %t.out \
 // RUN:   
--image=file=%t.amdgpu.bc,kind=hip,triple=amdgpu9.4-amd-amdhsa,arch=gfx9-4-generic:xnack+
 \
 // RUN:   
--image=file=%t.amdgpu.bc,kind=hip,triple=amdgpu12.00-amd-amdhsa,arch=gfx1200
+// RUN: llvm-offload-binary -o %t.single.out \
+// RUN:   
--image=file=%t.amdgpu.bc,kind=hip,triple=amdgpu12.00-amd-amdhsa,arch=gfx1200
 
 // Test that linker wrapper outputs .hipfb file without -r option for HIP 
non-RDC
 // The linker wrapper is called directly with the packaged device binary (not 
embedded in host object)
@@ -55,3 +57,70 @@ __attribute__((visibility("protected"), used)) int x;
 // RUN: test -s %t.gfx9-4-generic-xnack+.co
 // RUN: test -f %t.gfx1200.co
 // RUN: test -s %t.gfx1200.co
+
+// Emit one linked code object without bundling it into a HIP fat binary.
+// RUN: clang-linker-wrapper --host-triple=x86_64-unknown-linux-gnu \
+// RUN:   --emit-device-images-only --linker-path=/usr/bin/ld %t.single.out \
+// RUN:   -o %t.raw.co
+// RUN: llvm-readobj --file-headers %t.raw.co | FileCheck %s --check-prefix=RAW
+
+// RAW: Format: elf64-amdgpu
+
+// Emit each linked code object to the filename selected by the driver.
+// RUN: clang-linker-wrapper --emit-device-images-only \
+// RUN:   --device-image-output=amdgpu9.4-amd-amdhsa,gfx9-4-generic:xnack+ 
%t.raw-gfx9-4-generic-xnack+.co \
+// RUN:   --device-image-output=amdgpu12.00-amd-amdhsa,gfx1200 
%t.raw-gfx1200.co \
+// RUN:   --linker-path=/usr/bin/ld %t.out
+// RUN: test -f %t.raw-gfx1200.co
+// RUN: test -f %t.raw-gfx9-4-generic-xnack+.co
+
+// Clang passes the exact output filename for each device image.
+// RUN: rm -rf %t.dir && mkdir %t.dir
+// RUN: cd %t.dir && %clang -x hip --cuda-device-only \
+// RUN:   --no-gpu-bundle-output --offload-arch=gfx900 \
+// RUN:   --offload-arch=gfx1200 -nogpuinc -nogpulib %s
+// RUN: test -f 
%t.dir/linker-wrapper-hip-no-rdc-hip-amdgcn-amd-amdhsa-gfx900.out
+// RUN: test -f 
%t.dir/linker-wrapper-hip-no-rdc-hip-amdgcn-amd-amdhsa-gfx1200.out
+
+// The implicit filename keeps its target suffix for one architecture too.
+// RUN: rm -rf %t.single.dir && mkdir %t.single.dir
+// RUN: cd %t.single.dir && %clang -x hip --cuda-device-only \
+// RUN:   --no-gpu-bundle-output --offload-arch=gfx1200 \
+// RUN:   -nogpuinc -nogpulib %s
+// RUN: test -f 
%t.single.dir/linker-wrapper-hip-no-rdc-hip-amdgcn-amd-amdhsa-gfx1200.out
+
+// An explicit output name is invalid for multiple device images.
+// RUN: not clang-linker-wrapper --emit-device-images-only \
+// RUN:   --linker-path=/usr/bin/ld %t.out -o %t.multiple 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=MULTIPLE
+
+// MULTIPLE: error: cannot specify -o when emitting multiple device images
+
+// Every device image needs exactly one output mapping.
+// RUN: not clang-linker-wrapper --emit-device-images-only \
+// RUN:   --device-image-output=amdgpu12.00-amd-amdhsa,gfx1200 
%t.raw-gfx1200.co \
+// RUN:   --linker-path=/usr/bin/ld %t.out 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=MISSING-OUTPUT
+
+// MISSING-OUTPUT: error: expected an output file for each linked device image
+
+// RUN: not clang-linker-wrapper --emit-device-images-only \
+// RUN:   --device-image-output=amdgpu12.00-amd-amdhsa,gfx1200 
%t.raw-gfx1200.co \
+// RUN:   --device-image-output=amdgpu12.00-amd-amdhsa,gfx1200 
%t.raw-gfx1200-duplicate.co \
+// RUN:   --linker-path=/usr/bin/ld %t.single.out 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=DUPLICATE-OUTPUT
+
+// DUPLICATE-OUTPUT: error: duplicate output for device image 
'amdgpu12.00-amd-amdhsa,gfx1200'
+
+// RUN: not clang-linker-wrapper --emit-device-images-only \
+// RUN:   --device-image-output=amdgpu12.00-amd-amdhsa,gfx1200 
%t.raw-gfx1200.co \
+// RUN:   --linker-path=/usr/bin/ld %t.single.out -o %t.raw.co 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=OUTPUT-CONFLICT
+
+// OUTPUT-CONFLICT: error: cannot combine -o with explicit device image outputs
+
+// RUN: not clang-linker-wrapper --emit-fatbin-only \
+// RUN:   --emit-device-images-only --linker-path=/usr/bin/ld %t.single.out \
+// RUN:   -o %t.invalid 2>&1 | FileCheck %s --check-prefix=CONFLICT
+
+// CONFLICT: error: cannot emit a fat binary and raw device images together
diff --git a/clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp 
b/clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp
index c2de6578773c7..42c71791fbc96 100644
--- a/clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp
+++ b/clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp
@@ -17,6 +17,7 @@
 #include "clang/Basic/TargetID.h"
 #include "clang/Basic/Version.h"
 #include "llvm/ADT/MapVector.h"
+#include "llvm/ADT/StringMap.h"
 #include "llvm/BinaryFormat/Magic.h"
 #include "llvm/Bitcode/BitcodeWriter.h"
 #include "llvm/CodeGen/CommandFlags.h"
@@ -1079,13 +1080,24 @@ Error handleOverrideImages(
   return Error::success();
 }
 
+enum class DeviceOutputMode { Wrapped, FatBinary, Images };
+
+static Error writeOutputFile(StringRef Filename, StringRef Contents) {
+  Expected<std::unique_ptr<FileOutputBuffer>> FOBOrErr =
+      FileOutputBuffer::create(Filename, Contents.size());
+  if (!FOBOrErr)
+    return FOBOrErr.takeError();
+  std::unique_ptr<FileOutputBuffer> FOB = std::move(*FOBOrErr);
+  llvm::copy(Contents, FOB->getBufferStart());
+  return FOB->commit();
+}
+
 /// Transforms all the extracted offloading input files into an image that can
-/// be registered by the runtime. If NeedsWrapping is false, writes bundled
-/// output directly without wrapping or host linking.
+/// be registered by the runtime.
 Expected<SmallVector<StringRef>>
 linkAndWrapDeviceFiles(ArrayRef<SmallVector<OffloadFile>> LinkerInputFiles,
                        const InputArgList &Args, char **Argv, int Argc,
-                       bool NeedsWrapping) {
+                       DeviceOutputMode OutputMode) {
   llvm::TimeTraceScope TimeScope("Handle all device input");
 
   std::mutex ImageMtx;
@@ -1173,6 +1185,51 @@ 
linkAndWrapDeviceFiles(ArrayRef<SmallVector<OffloadFile>> LinkerInputFiles,
   if (Err)
     return std::move(Err);
 
+  if (OutputMode == DeviceOutputMode::Images) {
+    SmallVector<const OffloadingImage *> OutputImages;
+    for (auto &Entry : Images) {
+      for (const OffloadingImage &Image : Entry.second)
+        OutputImages.push_back(&Image);
+    }
+    if (OutputImages.empty())
+      return createStringError(
+          "expected at least one linked image for direct device image output");
+
+    bool HasExplicitOutput = Args.hasArg(OPT_o, OPT_out);
+    if (HasExplicitOutput && OutputImages.size() != 1)
+      return createStringError(
+          "cannot specify -o when emitting multiple device images");
+
+    StringMap<StringRef> DeviceOutputs;
+    for (const opt::Arg *Arg : Args.filtered(OPT_device_image_output)) {
+      if (!DeviceOutputs.try_emplace(Arg->getValue(0), 
Arg->getValue(1)).second)
+        return createStringError("duplicate output for device image '%s'",
+                                 Arg->getValue(0));
+    }
+    if (HasExplicitOutput && !DeviceOutputs.empty())
+      return createStringError(
+          "cannot combine -o with explicit device image outputs");
+    if (!HasExplicitOutput && DeviceOutputs.size() != OutputImages.size())
+      return createStringError(
+          "expected an output file for each linked device image");
+
+    for (const OffloadingImage *Image : OutputImages) {
+      SmallString<128> Target(Image->StringData.lookup("triple"));
+      Target += ",";
+      Target += Image->StringData.lookup("arch");
+      StringRef OutputFilename = HasExplicitOutput
+                                     ? StringRef(ExecutableName)
+                                     : DeviceOutputs.lookup(Target);
+      if (OutputFilename.empty())
+        return createStringError("missing output for device image '%s'",
+                                 Target.c_str());
+      if (Error E = writeOutputFile(OutputFilename, Image->Image->getBuffer()))
+        return std::move(E);
+    }
+
+    return SmallVector<StringRef>();
+  }
+
   // Create a binary image of each offloading image and either embed it into a
   // new object file, or if all inputs were direct offload binaries, emit the
   // fat binary directly (e.g. .hipfb / .fatbin).
@@ -1195,20 +1252,13 @@ 
linkAndWrapDeviceFiles(ArrayRef<SmallVector<OffloadFile>> LinkerInputFiles,
     if (!BundledImagesOrErr)
       return BundledImagesOrErr.takeError();
 
-    if (!NeedsWrapping) {
+    if (OutputMode == DeviceOutputMode::FatBinary) {
       if (BundledImagesOrErr->size() != 1)
         return createStringError(
             "Expected a single bundled image for direct fat binary output");
 
-      Expected<std::unique_ptr<FileOutputBuffer>> FOBOrErr =
-          FileOutputBuffer::create(
-              ExecutableName, BundledImagesOrErr->front()->getBufferSize());
-      if (!FOBOrErr)
-        return FOBOrErr.takeError();
-      std::unique_ptr<FileOutputBuffer> FOB = std::move(*FOBOrErr);
-      llvm::copy(BundledImagesOrErr->front()->getBuffer(),
-                 FOB->getBufferStart());
-      if (Error E = FOB->commit())
+      if (Error E = writeOutputFile(ExecutableName,
+                                    BundledImagesOrErr->front()->getBuffer()))
         return std::move(E);
 
       continue;
@@ -1571,19 +1621,25 @@ int main(int Argc, char **Argv) {
     if (!DeviceInputFiles)
       reportError(DeviceInputFiles.takeError());
 
-    // Check if we should emit fat binary directly without wrapping or host
-    // linking.
+    // Check if we should emit device output directly without host linking.
     bool EmitFatbinOnly = Args.hasArg(OPT_emit_fatbin_only);
-
-    // Link and process the device images. The function may emit a direct fat
-    // binary if --emit-fatbin-only is specified.
-    auto FilesOrErr = linkAndWrapDeviceFiles(*DeviceInputFiles, Args, Argv,
-                                             Argc, !EmitFatbinOnly);
+    bool EmitDeviceImagesOnly = Args.hasArg(OPT_emit_device_images_only);
+    if (EmitFatbinOnly && EmitDeviceImagesOnly)
+      reportError(createStringError(
+          "cannot emit a fat binary and raw device images together"));
+
+    DeviceOutputMode OutputMode = EmitFatbinOnly ? DeviceOutputMode::FatBinary
+                                  : EmitDeviceImagesOnly
+                                      ? DeviceOutputMode::Images
+                                      : DeviceOutputMode::Wrapped;
+
+    auto FilesOrErr =
+        linkAndWrapDeviceFiles(*DeviceInputFiles, Args, Argv, Argc, 
OutputMode);
     if (!FilesOrErr)
       reportError(FilesOrErr.takeError());
 
     // Run the host linking job with the rendered arguments.
-    if (!EmitFatbinOnly) {
+    if (OutputMode == DeviceOutputMode::Wrapped) {
       if (Error Err = runLinker(*FilesOrErr, Args))
         reportError(std::move(Err));
     }
diff --git a/clang/tools/clang-linker-wrapper/LinkerWrapperOpts.td 
b/clang/tools/clang-linker-wrapper/LinkerWrapperOpts.td
index 53b6c596de291..b34790b4a2350 100644
--- a/clang/tools/clang-linker-wrapper/LinkerWrapperOpts.td
+++ b/clang/tools/clang-linker-wrapper/LinkerWrapperOpts.td
@@ -69,6 +69,16 @@ def emit_fatbin_only
       Flags<[WrapperOnlyOption]>,
       HelpText<"Emit fat binary directly without wrapping or host linking">;
 
+def emit_device_images_only
+    : Flag<["--"], "emit-device-images-only">,
+      Flags<[WrapperOnlyOption]>,
+      HelpText<"Emit linked device images without bundling or host linking">;
+
+def device_image_output
+    : JoinedAndSeparate<["--"], "device-image-output=">,
+      Flags<[WrapperOnlyOption, HelpHidden]>,
+      MetaVarName<"<triple>,<arch> <file>">;
+
 def no_canonical_prefixes : Flag<["--"], "no-canonical-prefixes">,
   Flags<[WrapperOnlyOption]>,
   HelpText<"Do not resolve symbolic links, turn relative paths into absolute 
ones, or do anything else to identify the executable">;

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

Reply via email to