[clang] dc1273b - clang-linker-wrapper: Use AMDGPU::TargetID for device-image compatibility (#209135)
via cfe-commits
cfe-commits at lists.llvm.org
Mon Jul 13 06:05:07 PDT 2026
Author: Matt Arsenault
Date: 2026-07-13T15:05:03+02:00
New Revision: dc1273beba1c70cb74d03c760958ee625d8aecf5
URL: https://github.com/llvm/llvm-project/commit/dc1273beba1c70cb74d03c760958ee625d8aecf5
DIFF: https://github.com/llvm/llvm-project/commit/dc1273beba1c70cb74d03c760958ee625d8aecf5.diff
LOG: clang-linker-wrapper: Use AMDGPU::TargetID for device-image compatibility (#209135)
Previously the link compatibilty was based on a raw string match of the
triple + cpu name. Start parsing the triple, and use the compatibility
function. Start using AMDGPU::TargetID from TargetParser, as a step
towards removing clang's redundant TargetID handling.
Co-authored-by: Claude (Opus 4.8)
Added:
Modified:
clang/test/OffloadTools/clang-linker-wrapper/linker-wrapper.c
clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp
llvm/include/llvm/TargetParser/AMDGPUTargetParser.h
llvm/lib/Object/OffloadBinary.cpp
llvm/lib/TargetParser/AMDGPUTargetParser.cpp
llvm/unittests/TargetParser/TargetParserTest.cpp
Removed:
################################################################################
diff --git a/clang/test/OffloadTools/clang-linker-wrapper/linker-wrapper.c b/clang/test/OffloadTools/clang-linker-wrapper/linker-wrapper.c
index 0e018dc3af8c9..f4bac21181a9e 100644
--- a/clang/test/OffloadTools/clang-linker-wrapper/linker-wrapper.c
+++ b/clang/test/OffloadTools/clang-linker-wrapper/linker-wrapper.c
@@ -217,6 +217,63 @@ __attribute__((visibility("protected"), used)) int x;
// ARCH-ALL: clang{{.*}} -o {{.*}}.img -dumpdir a.out.amdgcn.gfx90a.img. --target=amdgcn-amd-amdhsa -mcpu=gfx90a -Wl,--no-undefined {{.*}}.o {{.*}}.o
// ARCH-ALL: clang{{.*}} -o {{.*}}.img -dumpdir a.out.amdgcn.gfx908.img. --target=amdgcn-amd-amdhsa -mcpu=gfx908 -Wl,--no-undefined {{.*}}.o {{.*}}.o
+// Two images with the same ISA but
diff erent triple spellings (the legacy
+// "amdgcn" alias and the canonical "amdgpu") must merge into a single device
+// link, not produce two separate device images.
+// RUN: llvm-offload-binary -o %t-legacy.out \
+// RUN: --image=file=%t.elf.o,kind=openmp,triple=amdgcn-amd-amdhsa,arch=gfx90a
+// RUN: %clang -cc1 %s -triple x86_64-unknown-linux-gnu -emit-obj -o %t-legacy.o -fembed-offload-object=%t-legacy.out
+// RUN: llvm-offload-binary -o %t-canon.out \
+// RUN: --image=file=%t.elf.o,kind=openmp,triple=amdgpu-amd-amdhsa,arch=gfx90a
+// RUN: %clang -cc1 %s -triple x86_64-unknown-linux-gnu -emit-obj -o %t-canon.o -fembed-offload-object=%t-canon.out
+// RUN: clang-linker-wrapper --host-triple=x86_64-unknown-linux-gnu --dry-run \
+// RUN: --linker-path=/usr/bin/ld %t-legacy.o %t-canon.o -o a.out 2>&1 | FileCheck %s --check-prefix=AMDGPU-TRIPLE-MERGE
+
+// AMDGPU-TRIPLE-MERGE: clang{{.*}} --target=amdgcn-amd-amdhsa -mcpu=gfx90a -Wl,--no-undefined {{.*}}.o {{.*}}.o
+// AMDGPU-TRIPLE-MERGE-NOT: clang{{.*}} --target=amdgcn-amd-amdhsa -mcpu=gfx90a -Wl,--no-undefined
+
+// Two images whose triples encode the ISA in the subarch (amdgpu9.0a-amd-amdhsa)
+// and carry no separate arch must merge into a single device link.
+// RUN: llvm-offload-binary -o %t-sub1.out \
+// RUN: --image=file=%t.elf.o,kind=openmp,triple=amdgpu9.0a-amd-amdhsa
+// RUN: %clang -cc1 %s -triple x86_64-unknown-linux-gnu -emit-obj -o %t-sub1.o -fembed-offload-object=%t-sub1.out
+// RUN: llvm-offload-binary -o %t-sub2.out \
+// RUN: --image=file=%t.elf.o,kind=openmp,triple=amdgpu9.0a-amd-amdhsa
+// RUN: %clang -cc1 %s -triple x86_64-unknown-linux-gnu -emit-obj -o %t-sub2.o -fembed-offload-object=%t-sub2.out
+// RUN: clang-linker-wrapper --host-triple=x86_64-unknown-linux-gnu --dry-run \
+// RUN: --linker-path=/usr/bin/ld %t-sub1.o %t-sub2.o -o a.out 2>&1 | FileCheck %s --check-prefix=AMDGPU-SUBARCH-MERGE
+
+// AMDGPU-SUBARCH-MERGE: clang{{.*}} --target=amdgpu9.0a-amd-amdhsa -Wl,--no-undefined {{.*}}.o {{.*}}.o
+// AMDGPU-SUBARCH-MERGE-NOT: clang{{.*}} --target=amdgpu9.0a-amd-amdhsa -Wl,--no-undefined
+
+// A major-family subarch triple (amdgpu9-amd-amdhsa) acts as a wildcard that
+// merges into a specific member of that family (amdgpu9.00-amd-amdhsa).
+// RUN: llvm-offload-binary -o %t-specific.out \
+// RUN: --image=file=%t.elf.o,kind=openmp,triple=amdgpu9.00-amd-amdhsa
+// RUN: %clang -cc1 %s -triple x86_64-unknown-linux-gnu -emit-obj -o %t-specific.o -fembed-offload-object=%t-specific.out
+// RUN: llvm-offload-binary -o %t-major.out \
+// RUN: --image=file=%t.elf.o,kind=openmp,triple=amdgpu9-amd-amdhsa
+// RUN: %clang -cc1 %s -triple x86_64-unknown-linux-gnu -emit-obj -o %t-major.o -fembed-offload-object=%t-major.out
+// RUN: clang-linker-wrapper --host-triple=x86_64-unknown-linux-gnu --dry-run \
+// RUN: --linker-path=/usr/bin/ld %t-specific.o %t-major.o -o a.out 2>&1 | FileCheck %s --check-prefix=AMDGPU-MAJOR-SUBARCH-MERGE
+
+// AMDGPU-MAJOR-SUBARCH-MERGE: clang{{.*}} --target=amdgpu9.00-amd-amdhsa -Wl,--no-undefined {{.*}}.o {{.*}}.o
+// AMDGPU-MAJOR-SUBARCH-MERGE-NOT: clang{{.*}} --target={{.*}}-amd-amdhsa -Wl,--no-undefined
+
+// Two distinct GPUs in the same major family are not interchangeable and must
+// not merge.
+// RUN: llvm-offload-binary -o %t-gfx900.out \
+// RUN: --image=file=%t.elf.o,kind=openmp,triple=amdgcn-amd-amdhsa,arch=gfx900
+// RUN: %clang -cc1 %s -triple x86_64-unknown-linux-gnu -emit-obj -o %t-gfx900.o -fembed-offload-object=%t-gfx900.out
+// RUN: llvm-offload-binary -o %t-gfx906.out \
+// RUN: --image=file=%t.elf.o,kind=openmp,triple=amdgcn-amd-amdhsa,arch=gfx906
+// RUN: %clang -cc1 %s -triple x86_64-unknown-linux-gnu -emit-obj -o %t-gfx906.o -fembed-offload-object=%t-gfx906.out
+// RUN: clang-linker-wrapper --host-triple=x86_64-unknown-linux-gnu --dry-run \
+// RUN: --linker-path=/usr/bin/ld %t-gfx900.o %t-gfx906.o -o a.out 2>&1 | FileCheck %s --check-prefix=AMDGPU-SAME-MAJOR-NO-MERGE
+
+// AMDGPU-SAME-MAJOR-NO-MERGE-DAG: clang{{.*}} -mcpu=gfx900 -Wl,--no-undefined {{.*}}.o
+// AMDGPU-SAME-MAJOR-NO-MERGE-DAG: clang{{.*}} -mcpu=gfx906 -Wl,--no-undefined {{.*}}.o
+
// RUN: llvm-offload-binary -o %t.out \
// RUN: --image=file=%t.elf.o,kind=openmp,triple=x86_64-unknown-linux-gnu \
// RUN: --image=file=%t.elf.o,kind=openmp,triple=x86_64-unknown-linux-gnu
diff --git a/clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp b/clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp
index 12b6cae3ea792..afa84eea41aae 100644
--- a/clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp
+++ b/clang/tools/clang-linker-wrapper/ClangLinkerWrapper.cpp
@@ -1414,8 +1414,7 @@ getDeviceInput(const ArgList &Args) {
OffloadFile::TargetID Target = Binary;
SmallVector<OffloadFile::TargetID> CompatibleTargets;
for (const auto &[ID, Input] : InputFiles)
- if (Target.first == ID.first &&
- clang::isCompatibleTargetID(Target.second, ID.second))
+ if (Target == ID || object::areTargetsCompatible(Target, ID))
CompatibleTargets.emplace_back(ID);
// Seed a new image when no existing target can provide for this input.
diff --git a/llvm/include/llvm/TargetParser/AMDGPUTargetParser.h b/llvm/include/llvm/TargetParser/AMDGPUTargetParser.h
index 4c1f17787e939..0fba0d4956d02 100644
--- a/llvm/include/llvm/TargetParser/AMDGPUTargetParser.h
+++ b/llvm/include/llvm/TargetParser/AMDGPUTargetParser.h
@@ -166,6 +166,10 @@ class LLVM_ABI TargetID {
TargetID(GPUKind Arch, const Triple &TT, TargetIDSetting XnackSetting,
TargetIDSetting SramEccSetting);
+ /// Construct a TargetID from a triple \p TT and the processor+features string
+ /// e.g. "gfx90a", "gfx90a:xnack+:sramecc-", "".
+ TargetID(const Triple &TT, StringRef TargetIDStr);
+
~TargetID() = default;
/// \return True if the current xnack setting is not "Unsupported".
@@ -234,6 +238,10 @@ class LLVM_ABI TargetID {
static std::optional<TargetID>
parseTargetIDString(StringRef TargetIDDirective);
+ /// Returns true if a device image built for *this can satisfy a request for
+ /// \p Other (i.e. they are compatible and can be grouped together).
+ bool isCompatibleWith(const TargetID &Other) const;
+
void print(raw_ostream &OS) const;
std::string toString() const;
diff --git a/llvm/lib/Object/OffloadBinary.cpp b/llvm/lib/Object/OffloadBinary.cpp
index e5c9f4dae131d..d1d01f9f7a667 100644
--- a/llvm/lib/Object/OffloadBinary.cpp
+++ b/llvm/lib/Object/OffloadBinary.cpp
@@ -22,6 +22,7 @@
#include "llvm/Object/ObjectFile.h"
#include "llvm/Support/Alignment.h"
#include "llvm/Support/SourceMgr.h"
+#include "llvm/TargetParser/AMDGPUTargetParser.h"
using namespace llvm;
using namespace llvm::object;
@@ -453,31 +454,25 @@ bool object::areTargetsCompatible(const OffloadFile::TargetID &LHS,
if (LHS == RHS)
return false;
- // The triples must match at all times.
- if (LHS.first != RHS.first)
- return false;
+ llvm::Triple LHSTT(LHS.first);
+ llvm::Triple RHSTT(RHS.first);
- // If the architecture is "all" we assume it is always compatible.
- if (LHS.second == "generic" || RHS.second == "generic")
- return true;
+ // The AMDGPU target requires target-id aware checks (base processor plus
+ // xnack/sramecc features), which also handle the generic wildcard and the
+ // legacy "amdgcn"/"amdgpu" triple spellings.
+ if (LHSTT.isAMDGPU()) {
+ AMDGPU::TargetID LTID(LHSTT, LHS.second);
+ AMDGPU::TargetID RTID(RHSTT, RHS.second);
+ return LTID.isCompatibleWith(RTID);
+ }
- // Only The AMDGPU target requires additional checks.
- llvm::Triple T(LHS.first);
- if (!T.isAMDGPU())
+ // For other targets the triples must be compatible.
+ if (!LHSTT.isCompatibleWith(RHSTT))
return false;
- // The base processor must always match.
- if (LHS.second.split(":").first != RHS.second.split(":").first)
- return false;
+ // If the architecture is "generic" we assume it is always compatible.
+ if (LHS.second == "generic" || RHS.second == "generic")
+ return true;
- // Check combintions of on / off features that must match.
- if (LHS.second.contains("xnack+") && RHS.second.contains("xnack-"))
- return false;
- if (LHS.second.contains("xnack-") && RHS.second.contains("xnack+"))
- return false;
- if (LHS.second.contains("sramecc-") && RHS.second.contains("sramecc+"))
- return false;
- if (LHS.second.contains("sramecc+") && RHS.second.contains("sramecc-"))
- return false;
- return true;
+ return LHS.second == RHS.second;
}
diff --git a/llvm/lib/TargetParser/AMDGPUTargetParser.cpp b/llvm/lib/TargetParser/AMDGPUTargetParser.cpp
index bf169c5b5ef42..c5f4247117d1b 100644
--- a/llvm/lib/TargetParser/AMDGPUTargetParser.cpp
+++ b/llvm/lib/TargetParser/AMDGPUTargetParser.cpp
@@ -11,7 +11,6 @@
//===----------------------------------------------------------------------===//
#include "llvm/TargetParser/AMDGPUTargetParser.h"
-#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringSwitch.h"
#include "llvm/ADT/Twine.h"
@@ -917,6 +916,28 @@ getTargetIDSettingFromFeatureString(StringRef FeatureString) {
llvm_unreachable("Malformed feature string");
}
+// Derive the architecture from the processor name in \p TargetIDStr. "generic"
+// and the empty processor name act as a wildcard.
+static GPUKind getGPUKindFromTargetID(const Triple &TT, StringRef TargetIDStr) {
+ StringRef CPUName = TargetIDStr.split(':').first;
+ return (CPUName.empty() || CPUName == "generic")
+ ? getGPUKindFromSubArch(TT.getSubArch())
+ : parseArchAMDGCN(CPUName);
+}
+
+TargetID::TargetID(const Triple &TT, StringRef TargetIDStr)
+ : TargetID(getGPUKindFromTargetID(TT, TargetIDStr), TT,
+ TargetIDSetting::Unsupported, TargetIDSetting::Unsupported) {
+ // Default xnack/sramecc to the "Any" wildcard when the architecture supports
+ // them, then apply any explicit feature overrides from the target-id string.
+ unsigned ArchAttr = getArchAttrAMDGCN(Arch);
+ if (ArchAttr & FEATURE_XNACK)
+ XnackSetting = TargetIDSetting::Any;
+ if (ArchAttr & FEATURE_SRAMECC)
+ SramEccSetting = TargetIDSetting::Any;
+ setTargetIDFromTargetIDStream(TargetIDStr);
+}
+
void TargetID::setTargetIDFromTargetIDStream(StringRef TargetID) {
SmallVector<StringRef, 3> TargetIDSplit;
TargetID.split(TargetIDSplit, ':');
@@ -942,40 +963,10 @@ TargetID::parseTargetIDString(StringRef TargetIDDirective) {
if (!TT.isAMDGCN())
return std::nullopt;
- SmallVector<StringRef, 3> FeatureSplit;
- Parts[4].split(FeatureSplit, ':');
- if (FeatureSplit.empty())
- return std::nullopt;
-
- StringRef CPUName = FeatureSplit[0];
-
- // Prefer the explicitly named processor so the parsed target id reflects it
- // (e.g. for validation against the triple subarch). The processor field may
- // be empty when the ISA is already encoded in the triple's subarch
- // (e.g. "amdgpu12.50-amd-amdhsa-unknown-"), in which case derive the arch
- // from the subarch.
- GPUKind Arch = CPUName.empty() ? getGPUKindFromSubArch(TT.getSubArch())
- : parseArchAMDGCN(CPUName);
-
- unsigned ArchAttr = getArchAttrAMDGCN(Arch);
-
- // Determine xnack/sramecc support based on the architecture attributes.
- TargetIDSetting XnackSetting = (ArchAttr & FEATURE_XNACK)
- ? TargetIDSetting::Any
- : TargetIDSetting::Unsupported;
- TargetIDSetting SramEccSetting = (ArchAttr & FEATURE_SRAMECC)
- ? TargetIDSetting::Any
- : TargetIDSetting::Unsupported;
-
- for (StringRef FeatureString :
- ArrayRef<StringRef>(FeatureSplit).drop_front(1)) {
- if (FeatureString.starts_with("xnack"))
- XnackSetting = getTargetIDSettingFromFeatureString(FeatureString);
- else if (FeatureString.starts_with("sramecc"))
- SramEccSetting = getTargetIDSettingFromFeatureString(FeatureString);
- }
-
- return TargetID(Arch, TT, XnackSetting, SramEccSetting);
+ // The processor+features field must be present, even if empty (the ISA can
+ // be encoded in the triple's subarch, e.g.
+ // "amdgpu12.50-amd-amdhsa-unknown-").
+ return TargetID(TT, Parts[4]);
}
void TargetID::print(raw_ostream &StreamRep) const {
@@ -1008,3 +999,26 @@ bool TargetID::operator==(const TargetID &Other) const {
SramEccSetting == Other.SramEccSetting && IsAMDHSA == Other.IsAMDHSA &&
TargetTripleString == Other.TargetTripleString;
}
+
+static bool areFeatureSettingsCompatible(TargetIDSetting A, TargetIDSetting B) {
+ return A == TargetIDSetting::Any || B == TargetIDSetting::Any || A == B;
+}
+
+bool TargetID::isCompatibleWith(const TargetID &Other) const {
+ // The triples must be compatible.
+ if (!Triple(getTargetTripleString())
+ .isCompatibleWith(Triple(Other.getTargetTripleString())))
+ return false;
+
+ // The processors must be compatible. A generic/major-family image (its
+ // subarch is the major-family subarch, or the GPU is unknown) acts as a
+ // wildcard that merges into a specific image group.
+ Triple::SubArchType SubA = getSubArch(Arch);
+ Triple::SubArchType SubB = getSubArch(Other.Arch);
+ if (!isSubArchCompatible(SubA, SubB))
+ return false;
+
+ // The xnack/sramecc settings must not conflict.
+ return areFeatureSettingsCompatible(XnackSetting, Other.XnackSetting) &&
+ areFeatureSettingsCompatible(SramEccSetting, Other.SramEccSetting);
+}
diff --git a/llvm/unittests/TargetParser/TargetParserTest.cpp b/llvm/unittests/TargetParser/TargetParserTest.cpp
index 231e3cb306298..1b6216f9ae68f 100644
--- a/llvm/unittests/TargetParser/TargetParserTest.cpp
+++ b/llvm/unittests/TargetParser/TargetParserTest.cpp
@@ -2828,4 +2828,52 @@ TEST(TargetParserTest, testAMDGPUgetIsaVersionFromSubArch) {
(AMDGPU::IsaVersion{0, 0, 0}));
}
+TEST(TargetParserTest, testAMDGPUTargetIDCompat) {
+ using AMDGPU::TargetID;
+ Triple AMDHSA("amdgcn-amd-amdhsa");
+ Triple AMDHSACanon("amdgpu-amd-amdhsa");
+
+ auto Compatible = [](const TargetID &A, const TargetID &B) {
+ // Compatibility is symmetric for the cases exercised here.
+ EXPECT_EQ(A.isCompatibleWith(B), B.isCompatibleWith(A));
+ return A.isCompatibleWith(B);
+ };
+
+ // Exact match is compatible.
+ EXPECT_TRUE(
+ Compatible(TargetID(AMDHSA, "gfx90a"), TargetID(AMDHSA, "gfx90a")));
+
+ // Different processors are not compatible.
+ EXPECT_FALSE(
+ Compatible(TargetID(AMDHSA, "gfx90a"), TargetID(AMDHSA, "gfx908")));
+
+ // "generic" and empty act as a wildcard that merges into a specific image,
+ // in both directions.
+ EXPECT_TRUE(
+ Compatible(TargetID(AMDHSA, "generic"), TargetID(AMDHSA, "gfx90a")));
+ EXPECT_TRUE(Compatible(TargetID(AMDHSA, ""), TargetID(AMDHSA, "gfx908")));
+
+ // Major-subarch wildcard triple merges into a specific processor.
+ Triple AMDHSAMajor9("amdgpu9-amd-amdhsa");
+ EXPECT_TRUE(
+ Compatible(TargetID(AMDHSAMajor9, ""), TargetID(AMDHSA, "gfx900")));
+
+ // Explicit On vs Off features must not match.
+ EXPECT_FALSE(Compatible(TargetID(AMDHSA, "gfx90a:xnack+"),
+ TargetID(AMDHSA, "gfx90a:xnack-")));
+ EXPECT_FALSE(Compatible(TargetID(AMDHSA, "gfx90a:sramecc+"),
+ TargetID(AMDHSA, "gfx90a:sramecc-")));
+
+ // "Any"/"Unsupported" settings act as wildcards: a plain gfx90a (xnack "Any")
+ // merges with an explicit xnack+ or xnack- request.
+ EXPECT_TRUE(Compatible(TargetID(AMDHSA, "gfx90a"),
+ TargetID(AMDHSA, "gfx90a:xnack+")));
+ EXPECT_TRUE(Compatible(TargetID(AMDHSA, "gfx90a"),
+ TargetID(AMDHSA, "gfx90a:xnack-")));
+
+ // The legacy "amdgcn" and canonical "amdgpu" triple spellings merge.
+ EXPECT_TRUE(
+ Compatible(TargetID(AMDHSA, "gfx90a"), TargetID(AMDHSACanon, "gfx90a")));
+}
+
} // namespace
More information about the cfe-commits
mailing list