[llvm-branch-commits] [lld] [llvm] [RFC][AMDGPU][lld] Add object linking support (PR #206787)

Shilei Tian via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Tue Jun 30 17:47:42 PDT 2026


https://github.com/shiltian updated https://github.com/llvm/llvm-project/pull/206787

>From e62ce247b2a300d9cf73cfcb94ab2d706958a4c4 Mon Sep 17 00:00:00 2001
From: Shilei Tian <i at tianshilei.me>
Date: Thu, 14 May 2026 08:55:14 -0400
Subject: [PATCH] [RFC][AMDGPU][lld] Add object linking support

Add AMDGPU ELF object-linking support in lld, including resource propagation,
LDS layout, indirect-call handling, named-barrier updates, target compatibility
checks, and kernel descriptor/metadata patching.

This is a large PR because the linker needs to understand and validate several
AMDGPU object-linking concepts end to end. I tried to keep the changes scoped to
the necessary linker support and related metadata plumbing, but I'm open to
suggestions on how to split or structure the review to make it easier.
---
 lld/ELF/AMDGPUObjectLinking.cpp               | 1914 +++++++++++++++++
 lld/ELF/AMDGPUObjectLinking.h                 |   25 +
 lld/ELF/Arch/AMDGPU.cpp                       |    2 +
 lld/ELF/CMakeLists.txt                        |    1 +
 lld/ELF/Driver.cpp                            |   15 +
 lld/ELF/InputFiles.cpp                        |   21 +
 lld/ELF/InputFiles.h                          |    4 +
 lld/ELF/Symbols.cpp                           |    1 +
 lld/ELF/Symbols.h                             |    8 +-
 .../ELF/amdgpu-lds-link-time-dynlds-alias.s   |  187 ++
 .../ELF/amdgpu-lds-link-time-dynlds-align.s   |  182 ++
 lld/test/ELF/amdgpu-lds-link-time-dynlds.s    |  184 ++
 lld/test/ELF/amdgpu-lds-link-time-grouped.s   |  425 ++++
 .../amdgpu-lds-link-time-indirect-cross-tu.s  |  203 ++
 .../ELF/amdgpu-lds-link-time-indirect-mixed.s |  180 ++
 .../ELF/amdgpu-lds-link-time-indirect-multi.s |  220 ++
 .../amdgpu-lds-link-time-indirect-nested.s    |  252 +++
 .../amdgpu-lds-link-time-indirect-prototype.s |  245 +++
 .../amdgpu-lds-link-time-indirect-resource.s  |  176 ++
 .../ELF/amdgpu-lds-link-time-instructions.s   |  164 ++
 lld/test/ELF/amdgpu-lds-link-time-layout.s    |  119 +
 .../ELF/amdgpu-lds-link-time-named-barrier.s  |  260 +++
 .../amdgpu-lds-link-time-ordering-complex.s   |  402 ++++
 .../amdgpu-lds-link-time-ordering-frontier.s  |  112 +
 ...amdgpu-lds-link-time-ordering-multigroup.s |  505 +++++
 .../ELF/amdgpu-lds-link-time-ordering-sizes.s |  209 ++
 .../ELF/amdgpu-lds-link-time-ordering-tiers.s |  313 +++
 lld/test/ELF/amdgpu-lds-link-time-padding.s   |   82 +
 .../amdgpu-lds-link-time-per-kernel-sizes.s   |  240 +++
 .../ELF/amdgpu-lds-link-time-random-layout.s  |  432 ++++
 .../ELF/amdgpu-lds-link-time-recursive-scc.s  |  161 ++
 lld/test/ELF/amdgpu-named-barrier-cross-tu.s  |  193 ++
 lld/test/ELF/amdgpu-named-barrier-grouped.s   |  109 +
 .../ELF/amdgpu-object-linking-asm-roundtrip.s |  145 ++
 .../amdgpu-object-linking-malformed-strtab.s  |  209 ++
 .../ELF/amdgpu-object-linking-target-id.s     |  226 ++
 .../ELF/amdgpu-object-linking-wave-size.s     |  129 ++
 lld/test/ELF/amdgpu-resource-usage-alias.s    |  173 ++
 .../ELF/amdgpu-resource-usage-recursive-scc.s |  257 +++
 .../ELF/amdgpu-resource-usage-section-sym.s   |  236 ++
 .../ELF/amdgpu-resource-usage-stale-info.s    |  235 ++
 lld/test/ELF/amdgpu-resource-usage.s          |  400 ++++
 llvm/include/llvm/Support/AMDGPUIsaInfo.h     |   95 +
 .../llvm/Support/AMDGPUObjLinkingInfo.h       |    3 +
 llvm/lib/Target/AMDGPU/AMDGPUAsmPrinter.cpp   |    1 +
 .../AMDGPU/AMDGPUHSAMetadataStreamer.cpp      |    3 +-
 .../AMDGPU/AsmParser/AMDGPUAsmParser.cpp      |    6 +
 .../MCTargetDesc/AMDGPUTargetStreamer.cpp     |   10 +-
 .../MCTargetDesc/AMDGPUTargetStreamer.h       |    7 +
 .../Target/AMDGPU/Utils/AMDGPUBaseInfo.cpp    |   21 +
 50 files changed, 9698 insertions(+), 4 deletions(-)
 create mode 100644 lld/ELF/AMDGPUObjectLinking.cpp
 create mode 100644 lld/ELF/AMDGPUObjectLinking.h
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-dynlds-alias.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-dynlds-align.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-dynlds.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-grouped.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-indirect-cross-tu.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-indirect-mixed.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-indirect-multi.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-indirect-nested.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-indirect-prototype.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-indirect-resource.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-instructions.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-layout.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-named-barrier.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-ordering-complex.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-ordering-frontier.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-ordering-multigroup.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-ordering-sizes.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-ordering-tiers.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-padding.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-per-kernel-sizes.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-random-layout.s
 create mode 100644 lld/test/ELF/amdgpu-lds-link-time-recursive-scc.s
 create mode 100644 lld/test/ELF/amdgpu-named-barrier-cross-tu.s
 create mode 100644 lld/test/ELF/amdgpu-named-barrier-grouped.s
 create mode 100644 lld/test/ELF/amdgpu-object-linking-asm-roundtrip.s
 create mode 100644 lld/test/ELF/amdgpu-object-linking-malformed-strtab.s
 create mode 100644 lld/test/ELF/amdgpu-object-linking-target-id.s
 create mode 100644 lld/test/ELF/amdgpu-object-linking-wave-size.s
 create mode 100644 lld/test/ELF/amdgpu-resource-usage-alias.s
 create mode 100644 lld/test/ELF/amdgpu-resource-usage-recursive-scc.s
 create mode 100644 lld/test/ELF/amdgpu-resource-usage-section-sym.s
 create mode 100644 lld/test/ELF/amdgpu-resource-usage-stale-info.s
 create mode 100644 lld/test/ELF/amdgpu-resource-usage.s
 create mode 100644 llvm/include/llvm/Support/AMDGPUIsaInfo.h

diff --git a/lld/ELF/AMDGPUObjectLinking.cpp b/lld/ELF/AMDGPUObjectLinking.cpp
new file mode 100644
index 0000000000000..435b13784733c
--- /dev/null
+++ b/lld/ELF/AMDGPUObjectLinking.cpp
@@ -0,0 +1,1914 @@
+//===- AMDGPUObjectLinking.cpp - AMDGPU link-time resolution --------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Implements link-time resolution and patching for AMDGPU object linking.
+//
+// The linker:
+//   1. Validates target-ID compatibility for participating objects
+//   2. Collects SHN_AMDGPU_LDS and named-barrier symbols
+//   3. Parses .amdgpu.info to build the cross-TU call graph, type-ID
+//      signatures, LDS and named-barrier uses, and per-function resource usage
+//   4. Resolves indirect call edges, function aliases, and kernel entries
+//   5. Validates call-edge wave-size compatibility
+//   6. Builds a shared SCC condensation graph for kernel-reachable functions
+//   7. Computes per-kernel LDS and named-barrier reachability
+//   8. Assigns LDS offsets and named-barrier IDs
+//   9. Propagates resource usage across the SCC graph (MAX for registers, OR
+//      for flags, caller scratch plus maximum callee scratch path)
+//  10. Validates required ABI occupancy metadata, call-edge occupancy
+//      compatibility, and each kernel's LDS usage against its occupancy
+//
+// After resolution, the linker patches kernel descriptors and HSA metadata
+// with the resolved LDS size, named-barrier count, propagated register usage,
+// scratch size, dynamic-stack flag, and related resource fields.
+//
+//===----------------------------------------------------------------------===//
+
+#include "AMDGPUObjectLinking.h"
+#include "Config.h"
+#include "InputFiles.h"
+#include "InputSection.h"
+#include "SymbolTable.h"
+#include "Symbols.h"
+#include "lld/Common/ErrorHandler.h"
+#include "llvm/BinaryFormat/AMDGPUMetadataVerifier.h"
+#include "llvm/BinaryFormat/ELF.h"
+#include "llvm/BinaryFormat/MsgPackDocument.h"
+#include "llvm/MC/MCSubtargetInfo.h"
+#include "llvm/MC/TargetRegistry.h"
+#include "llvm/Support/AMDGPUIsaInfo.h"
+#include "llvm/Support/AMDGPUObjLinkingInfo.h"
+#include "llvm/Support/AMDHSAKernelDescriptor.h"
+#include "llvm/Support/Alignment.h"
+#include "llvm/Support/Allocator.h"
+#include "llvm/Support/Debug.h"
+#include "llvm/Support/Endian.h"
+#include "llvm/Support/TimeProfiler.h"
+#include "llvm/TargetParser/Triple.h"
+
+#define DEBUG_TYPE "amdgpu-object-linking"
+
+using namespace llvm;
+using namespace llvm::support::endian;
+using namespace llvm::object;
+using namespace llvm::ELF;
+using namespace lld;
+using namespace lld::elf;
+
+namespace llvm::AMDGPU {
+StringRef getArchNameFromElfMach(unsigned elfMach);
+} // namespace llvm::AMDGPU
+
+namespace {
+
+// One SHN_AMDGPU_LDS symbol after collection and before it is rewritten to a
+// concrete offset.
+struct LDSSymbolInfo {
+  Symbol *sym = nullptr;
+  uint64_t size = 0;
+  Align alignment = Align(1);
+  uint64_t assignedOffset = 0;
+};
+
+// One named-barrier pseudo-symbol. The linker assigns each barrier a hardware
+// ID range and rewrites the symbol to the encoded barrier address.
+struct NamedBarrierInfo {
+  Symbol *sym = nullptr;
+  uint32_t slotCount = 0;
+  uint32_t assignedBarId = 0;
+};
+
+// Target-independent resource usage for one function or SCC. This is the raw
+// data parsed from .amdgpu.info, plus the same fields after call-graph
+// propagation. Occupancy and wave size are validation inputs and are only
+// meaningful for direct (local) records.
+struct ResourceInfo {
+  uint32_t numArchVGPR = 0;
+  uint32_t numAccVGPR = 0;
+  uint32_t numSGPR = 0;
+  uint32_t scratchSize = 0;
+  uint32_t occupancy = 0;
+  uint32_t waveSize = 0;
+  bool usesVCC = false;
+  bool usesFlatScratch = false;
+  bool hasDynSizedStack = false;
+};
+
+// One function in the device call graph, plus the data computed for that
+// function when it is a kernel entry.
+struct CGNode {
+  Symbol *sym = nullptr;
+  bool isKernel = false;
+
+  SmallVector<CGNode *, 4> callees;
+  SmallVector<size_t, 2> ldsUseIndices;
+  SmallVector<size_t, 2> barrierUseIndices;
+
+  ResourceInfo localRes;
+  bool hasLocalRes = false;
+
+  ResourceInfo propagatedRes;
+  bool hasPropagatedRes = false;
+
+  DenseSet<size_t> reachableLDS;
+  DenseSet<size_t> reachableBarriers;
+  uint32_t ldsSize = 0;
+  uint32_t numNamedBarrier = 0;
+};
+
+// Functions and indirect callers that share the same type-id encoding.
+struct SignatureInfo {
+  SmallVector<CGNode *, 4> functions;
+  SmallVector<CGNode *, 4> indirectCallers;
+};
+
+// One strongly connected component in the kernel-reachable call graph. The SCC
+// carries direct and propagated reachability/resource data so LDS,
+// named-barrier, and resource passes can share the same condensed graph.
+struct CallGraphSCC {
+  SmallVector<CGNode *, 4> members;
+  SmallVector<unsigned, 4> callees;
+
+  DenseSet<size_t> reachableLDS;
+  DenseSet<size_t> reachableBarriers;
+
+  ResourceInfo localRes;
+  ResourceInfo propagatedRes;
+
+  bool hasPropagatedRes = false;
+  bool isRecursive = false;
+};
+
+// Cross-object device call graph built from .amdgpu.info records. The graph
+// keeps kernel order stable for diagnostics and later per-kernel patching.
+// After construction, the graph can be condensed into an SCC DAG for
+// reachability and resource propagation.
+class AMDGPUCallGraph {
+  SpecificBumpPtrAllocator<CGNode> alloc;
+  DenseMap<Symbol *, CGNode *> symToNode;
+  SmallVector<CGNode *> kernelNodes;
+  bool hasLDSUseEntries = false;
+  bool hasBarrierUseEntries = false;
+
+  DenseSet<CGNode *> addressTakenNodes;
+  StringMap<SignatureInfo> signatureMap;
+
+  // SCC condensation state (populated by buildCondensedGraph).
+  SmallVector<CallGraphSCC, 16> sccs;
+  DenseMap<CGNode *, unsigned> nodeToSCC;
+
+public:
+  CGNode &getOrCreate(Symbol *sym) {
+    auto [it, inserted] = symToNode.try_emplace(sym, nullptr);
+    if (inserted) {
+      it->second = new (alloc.Allocate()) CGNode();
+      it->second->sym = sym;
+    }
+    return *it->second;
+  }
+
+  CGNode *lookup(Symbol *sym) const {
+    auto it = symToNode.find(sym);
+    return it != symToNode.end() ? it->second : nullptr;
+  }
+
+  void addKernel(CGNode &node) {
+    if (node.isKernel)
+      return;
+    node.isKernel = true;
+    kernelNodes.push_back(&node);
+  }
+
+  void markAddressTaken(CGNode *node) { addressTakenNodes.insert(node); }
+
+  void addIndirectCall(CGNode *caller, StringRef encoding) {
+    signatureMap[encoding].indirectCallers.push_back(caller);
+  }
+
+  void addSignature(CGNode *node, StringRef encoding) {
+    signatureMap[encoding].functions.push_back(node);
+  }
+
+  // After all sections are parsed, resolve indirect call edges by matching
+  // signature encodings: for each indirect call encoding, the potential callees
+  // are address-taken functions with the same encoding.
+  void buildIndirectEdges() {
+    for (auto &[encoding, info] : signatureMap) {
+      if (info.indirectCallers.empty())
+        continue;
+
+      SmallVector<CGNode *, 4> potentialCallees;
+      for (CGNode *func : info.functions) {
+        if (addressTakenNodes.count(func))
+          potentialCallees.push_back(func);
+      }
+
+      if (potentialCallees.empty())
+        continue;
+
+      for (CGNode *caller : info.indirectCallers) {
+        for (CGNode *callee : potentialCallees) {
+          LLVM_DEBUG(dbgs() << "  indirect edge: " << caller->sym->getName()
+                            << " -> " << callee->sym->getName()
+                            << " (sig=" << encoding << ")\n");
+          caller->callees.push_back(callee);
+        }
+      }
+    }
+  }
+
+  // Resolve function aliases: multiple ELF symbols can point to the same
+  // address (e.g. weak/strong pairs or constructor variants). The compiler
+  // emits .amdgpu.info only for one of them. Redirect callee pointers from
+  // alias nodes (no local resource info) to their canonical definition so
+  // that reachability and resource propagation see real data.
+  void resolveAliases() {
+    DenseMap<std::pair<SectionBase *, uint64_t>, CGNode *> addrToNode;
+    for (auto &[sym, node] : symToNode) {
+      if (!node->hasLocalRes)
+        continue;
+      auto *d = dyn_cast<Defined>(sym);
+      if (!d || !d->section)
+        continue;
+      addrToNode[{d->section, d->value}] = node;
+    }
+
+    DenseMap<CGNode *, CGNode *> aliasMap;
+    for (auto &[sym, node] : symToNode) {
+      if (node->hasLocalRes)
+        continue;
+      auto *d = dyn_cast<Defined>(sym);
+      if (!d || !d->section)
+        continue;
+      auto it = addrToNode.find({d->section, d->value});
+      if (it != addrToNode.end() && it->second != node) {
+        LLVM_DEBUG(dbgs() << "  alias: " << sym->getName() << " -> "
+                          << it->second->sym->getName() << "\n");
+        aliasMap[node] = it->second;
+      }
+    }
+
+    if (aliasMap.empty())
+      return;
+
+    for (auto &[sym, node] : symToNode) {
+      for (CGNode *&callee : node->callees) {
+        if (auto it = aliasMap.find(callee); it != aliasMap.end())
+          callee = it->second;
+      }
+    }
+
+    for (CGNode *&k : kernelNodes) {
+      if (auto it = aliasMap.find(k); it != aliasMap.end())
+        k = it->second;
+    }
+  }
+
+  void setHasLDSUses() { hasLDSUseEntries = true; }
+  bool hasLDSUses() const { return hasLDSUseEntries; }
+
+  void setHasBarrierUses() { hasBarrierUseEntries = true; }
+  bool hasBarrierUses() const { return hasBarrierUseEntries; }
+
+  ArrayRef<CGNode *> kernels() const { return kernelNodes; }
+
+  using const_iterator = DenseMap<Symbol *, CGNode *>::const_iterator;
+  const_iterator begin() const { return symToNode.begin(); }
+  const_iterator end() const { return symToNode.end(); }
+
+  // Build the kernel-reachable SCC DAG. Tarjan emits SCCs in reverse
+  // topological order so every outgoing edge points to an earlier SCC and
+  // consumers can use a single forward sweep.
+  bool buildCondensedGraph(Ctx &ctx, bool validateResources);
+
+  // Propagate transitive LDS/barrier reachability over the SCC DAG into each
+  // kernel node.
+  void computeKernelReachability();
+
+  // Propagate resource usage over the SCC DAG. Since SCCs are in reverse
+  // topological order, each callee has already been propagated when its
+  // caller is visited.
+  void propagateResourceUsage();
+
+private:
+  bool buildSCCs(Ctx &ctx, CGNode *node,
+                 DenseMap<CGNode *, unsigned> &nodeIndex,
+                 DenseMap<CGNode *, unsigned> &lowLink,
+                 DenseSet<CGNode *> &onStack, SmallVectorImpl<CGNode *> &stack,
+                 unsigned &nextIndex, bool validateResources);
+
+  void buildSCCEdges();
+};
+
+// The relocation payload needed to resolve a tagged .amdgpu.info entry.
+struct RelocInfo {
+  uint32_t symIdx = 0;
+  int64_t addend = 0;
+};
+
+struct LDSAllocationTraits {
+  bool hasUses(const AMDGPUCallGraph &cg) const { return cg.hasLDSUses(); }
+
+  const DenseSet<size_t> &reachableResources(const CGNode &kernel) const {
+    return kernel.reachableLDS;
+  }
+
+  uint64_t initialValue() const { return 0; }
+  uint64_t size(const LDSSymbolInfo &lds) const { return lds.size; }
+  Align alignment(const LDSSymbolInfo &lds) const { return lds.alignment; }
+  uint64_t assigned(const LDSSymbolInfo &lds) const {
+    return lds.assignedOffset;
+  }
+
+  void setAssigned(LDSSymbolInfo &lds, uint64_t value) const {
+    lds.assignedOffset = value;
+  }
+
+  bool compare(const LDSSymbolInfo &a, const LDSSymbolInfo &b) const {
+    if (a.alignment != b.alignment)
+      return a.alignment > b.alignment;
+    if (a.size != b.size)
+      return a.size > b.size;
+    return a.sym->getName() < b.sym->getName();
+  }
+
+  void printAllocation(const LDSSymbolInfo &lds, uint64_t offset,
+                       size_t users) const {
+    LLVM_DEBUG(dbgs() << "    allocate " << lds.sym->getName() << " at "
+                      << offset << " size=" << lds.size << " users=" << users
+                      << "\n");
+  }
+};
+
+struct NamedBarrierAllocationTraits {
+  bool hasUses(const AMDGPUCallGraph &cg) const { return cg.hasBarrierUses(); }
+
+  const DenseSet<size_t> &reachableResources(const CGNode &kernel) const {
+    return kernel.reachableBarriers;
+  }
+
+  uint64_t initialValue() const { return 1; }
+  uint64_t size(const NamedBarrierInfo &bar) const { return bar.slotCount; }
+  Align alignment(const NamedBarrierInfo &) const { return Align(1); }
+  uint64_t assigned(const NamedBarrierInfo &bar) const {
+    return bar.assignedBarId;
+  }
+
+  void setAssigned(NamedBarrierInfo &bar, uint64_t value) const {
+    bar.assignedBarId = static_cast<uint32_t>(value);
+  }
+
+  bool compare(const NamedBarrierInfo &a, const NamedBarrierInfo &b) const {
+    if (a.slotCount != b.slotCount)
+      return a.slotCount > b.slotCount;
+    return a.sym->getName() < b.sym->getName();
+  }
+
+  void printAllocation(const NamedBarrierInfo &bar, uint64_t barId,
+                       size_t users) const {
+    LLVM_DEBUG(dbgs() << "    allocate " << bar.sym->getName() << " at "
+                      << barId << " slots=" << bar.slotCount
+                      << " users=" << users << "\n");
+  }
+};
+
+// Final target-specific resource values for one kernel after propagation.
+// These are derived from ResourceInfo plus the linked target, kernel descriptor
+// conventions, and resolved named-barrier allocation. Both kernel descriptors
+// and HSA metadata consume this so their formulas cannot drift.
+struct ResolvedKernelResources {
+  uint32_t totalVGPR = 0;
+  uint32_t totalSGPR = 0;
+  uint32_t numAccVGPR = 0;
+  uint32_t scratchSize = 0;
+  uint32_t accumOffset = 0;
+  uint32_t namedBarCnt = 0;
+  bool scratchEnable = false;
+  bool usesDynamicStack = false;
+};
+
+} // namespace
+
+//===----------------------------------------------------------------------===//
+// LDS symbol collection
+//===----------------------------------------------------------------------===//
+
+// Collect every distinct SHN_AMDGPU_LDS common symbol that needs a final
+// link-time offset. Named barrier symbols (identified by the
+// __amdgpu_named_barrier prefix) are routed to a separate barriers array.
+static void collectLDSSymbols(Ctx &ctx,
+                              SmallVectorImpl<LDSSymbolInfo> &ldsSymbols,
+                              SmallVectorImpl<NamedBarrierInfo> &barriers) {
+  DenseSet<Symbol *> seen;
+  for (ELFFileBase *file : ctx.objectFiles) {
+    if (!file->hasCommonSyms)
+      continue;
+    for (Symbol *sym : file->getGlobalSymbols()) {
+      if (!sym->isAMDGPULDS || !seen.insert(sym).second)
+        continue;
+      auto *cs = dyn_cast<CommonSymbol>(sym);
+      if (!cs)
+        continue;
+      if (sym->getName().starts_with("__amdgpu_named_barrier")) {
+        LLVM_DEBUG(dbgs() << "  collected named barrier: " << sym->getName()
+                          << " size=" << cs->size << "\n");
+        barriers.push_back({sym, static_cast<uint32_t>(cs->size / 16)});
+      } else {
+        LLVM_DEBUG(dbgs() << "  collected LDS symbol: " << sym->getName()
+                          << " size=" << cs->size << " align=" << cs->alignment
+                          << "\n");
+        ldsSymbols.push_back(
+            {sym, cs->size, Align(cs->alignment), /*assignedOffset=*/0});
+      }
+    }
+  }
+}
+
+// Return the placement frontier shared by all kernels in users. Each kernel has
+// an independent frontier, and a resource used by several kernels must be
+// placed after every one of those kernels' existing live resources.
+// initialValue is the resource-specific base frontier, e.g. LDS offsets start
+// at 0 while named-barrier IDs start at 1.
+static uint64_t
+getMaxKernelFrontier(ArrayRef<CGNode *> users,
+                     const DenseMap<CGNode *, uint64_t> &kernelFrontiers,
+                     uint64_t initialValue) {
+  uint64_t frontier = initialValue;
+  for (CGNode *user : users) {
+    auto it = kernelFrontiers.find(user);
+    assert(it != kernelFrontiers.end() && "missing kernel frontier");
+    frontier = std::max(frontier, it->second);
+  }
+  return frontier;
+}
+
+// Assign one packed, deterministic layout used by every kernel. This is the
+// fallback when the input objects do not provide .amdgpu.info reachability for
+// a resource kind. Non-zero-sized resources advance a single global frontier.
+// Zero-sized resources are assigned to the first suitably aligned address after
+// the fixed layout, matching the dynamic-LDS convention while also giving
+// zero-slot barriers a stable value if they ever appear.
+template <typename ResourceT, typename Traits>
+static void assignUniversalResources(SmallVectorImpl<ResourceT> &resources,
+                                     const Traits &traits) {
+  llvm::sort(resources, [&](const ResourceT &a, const ResourceT &b) {
+    return traits.compare(a, b);
+  });
+  uint64_t frontier = traits.initialValue();
+  for (ResourceT &resource : resources) {
+    if (traits.size(resource) == 0)
+      continue;
+    frontier = alignTo(frontier, traits.alignment(resource));
+    traits.setAssigned(resource, frontier);
+    frontier += traits.size(resource);
+  }
+  Align maxZeroAlign(1);
+  for (ResourceT &resource : resources) {
+    if (traits.size(resource) == 0)
+      maxZeroAlign = std::max(maxZeroAlign, traits.alignment(resource));
+  }
+  uint64_t zeroBase = alignTo(frontier, maxZeroAlign);
+  for (ResourceT &resource : resources) {
+    if (traits.size(resource) == 0)
+      traits.setAssigned(resource, zeroBase);
+  }
+}
+
+#ifndef NDEBUG
+template <typename ResourceT, typename Traits>
+static void assertResourceLayoutInvariants(
+    ArrayRef<ResourceT> resources, ArrayRef<CGNode *> kernels,
+    const DenseMap<size_t, SmallVector<CGNode *, 4>> &resourceToUsers,
+    const DenseSet<size_t> &assigned, const Traits &traits) {
+  assert(assigned.size() == resources.size() && "not all resources assigned");
+  for (size_t idx = 0, e = resources.size(); idx < e; ++idx)
+    assert(assigned.count(idx) && "resource not assigned");
+
+  for (const auto &[idx, users] : resourceToUsers) {
+    uint64_t start = traits.assigned(resources[idx]);
+    for (CGNode *user : users) {
+      assert(traits.reachableResources(*user).count(idx) &&
+             "inconsistent resource user map");
+      assert(traits.assigned(resources[idx]) == start &&
+             "resource has non-uniform assignment across kernels");
+    }
+  }
+
+  for (CGNode *kernel : kernels) {
+    SmallVector<std::pair<uint64_t, uint64_t>, 8> intervals;
+    for (size_t idx : traits.reachableResources(*kernel)) {
+      uint64_t size = traits.size(resources[idx]);
+      if (size == 0)
+        continue;
+      uint64_t begin = traits.assigned(resources[idx]);
+      intervals.push_back({begin, begin + size});
+    }
+
+    llvm::sort(intervals);
+    for (size_t i = 1, e = intervals.size(); i < e; ++i)
+      assert(intervals[i - 1].second <= intervals[i].first &&
+             "kernel has overlapping resource intervals");
+  }
+}
+#endif
+
+// Assign a single global resource range to each resource while minimizing the
+// per-kernel high-water mark. ResourceT is the linker-side record for one
+// allocatable entity. It is currently either LDSSymbolInfo, where the assigned
+// value is an LDS byte offset, or NamedBarrierInfo, where the assigned value is
+// the first hardware named-barrier ID in the resource's ID range.
+//
+// The algorithm uses a per-kernel frontier. For a resource used by kernels K,
+// place it at the maximum current frontier of K, rounded up to the resource's
+// alignment, then advance only those kernels by the resource size. This keeps
+// one uniform assignment per resource, while allowing resources reached by
+// disjoint kernel sets to overlap. Resources are assigned in descending number
+// of reaching kernels, with Traits::compare breaking ties in a
+// resource-specific way.
+//
+// Traits supplies the resource-specific policy:
+//   - hasUses(cg): whether .amdgpu.info reachability data is available.
+//   - reachableResources(kernel): the DenseSet<size_t> of resource indices
+//     reachable from the kernel.
+//   - initialValue(): the first valid frontier value, e.g. 0 for LDS offsets
+//     and 1 for named-barrier IDs.
+//   - size(resource): the amount by which a non-zero resource advances a
+//     kernel frontier.
+//   - alignment(resource): required alignment of the assigned value.
+//   - assigned(resource) / setAssigned(resource, value): accessors for the
+//     resource's final assignment.
+//   - compare(a, b): deterministic allocation priority for equal user counts.
+//   - printAllocation(resource, value, users): debug logging for claimed
+//     resources.
+//
+// If reachability is absent, all resources are laid out universally using the
+// same Traits policy. If a resource is unreachable from every kernel, it still
+// receives a valid assignment for relocation resolution, but it does not affect
+// any kernel frontier.
+template <typename ResourceT, typename Traits>
+static void assignGroupedResources(SmallVectorImpl<ResourceT> &resources,
+                                   AMDGPUCallGraph &cg, const Traits &traits) {
+  if (!traits.hasUses(cg)) {
+    assignUniversalResources(resources, traits);
+    return;
+  }
+
+  SmallVector<CGNode *, 8> kernels(cg.kernels().begin(), cg.kernels().end());
+  DenseMap<size_t, SmallVector<CGNode *, 4>> resourceToUsers;
+  DenseMap<CGNode *, uint64_t> kernelFrontiers;
+  for (CGNode *kernel : kernels) {
+    kernelFrontiers[kernel] = traits.initialValue();
+    for (size_t idx : traits.reachableResources(*kernel))
+      resourceToUsers[idx].push_back(kernel);
+  }
+
+  auto idxCmp = [&](size_t a, size_t b) {
+    return traits.compare(resources[a], resources[b]);
+  };
+  auto getUserCount = [&](size_t idx) {
+    auto it = resourceToUsers.find(idx);
+    return it == resourceToUsers.end() ? 0 : it->second.size();
+  };
+  auto claimedCmp = [&](size_t a, size_t b) {
+    size_t aUsers = getUserCount(a);
+    size_t bUsers = getUserCount(b);
+    if (aUsers != bUsers)
+      return aUsers > bUsers;
+    return idxCmp(a, b);
+  };
+
+  DenseSet<size_t> assigned;
+  // Split resources by two independent properties:
+  //   - claimed resources are reachable from at least one kernel and therefore
+  //     participate in per-kernel frontier allocation; unclaimed resources only
+  //     need a valid standalone assignment for relocation resolution.
+  //   - non-zero resources consume frontier space; zero-sized resources get a
+  //     stable aligned assignment but must not advance any kernel frontier.
+  //     In practice, zero-sized resources are dynamic LDS symbols. Named
+  //     barriers should have nonzero slot counts, but the shared allocator
+  //     keeps the zero-size handling generic.
+  SmallVector<size_t, 8> claimedNonZero;
+  SmallVector<size_t, 4> claimedZeroSize;
+  SmallVector<size_t, 8> unclaimedNonZero;
+  SmallVector<size_t, 4> unclaimedZeroSize;
+
+  for (size_t i = 0, e = resources.size(); i < e; ++i) {
+    bool hasUsers = getUserCount(i) != 0;
+    if (hasUsers && traits.size(resources[i]) != 0)
+      claimedNonZero.push_back(i);
+    else if (hasUsers)
+      claimedZeroSize.push_back(i);
+    else if (traits.size(resources[i]) == 0)
+      unclaimedZeroSize.push_back(i);
+    else
+      unclaimedNonZero.push_back(i);
+  }
+
+  llvm::sort(claimedNonZero, claimedCmp);
+  llvm::sort(claimedZeroSize, claimedCmp);
+
+  auto assignResource = [&](size_t idx, uint64_t start) {
+    traits.setAssigned(resources[idx], start);
+    [[maybe_unused]] bool inserted = assigned.insert(idx).second;
+    assert(inserted && "resource assigned multiple times");
+  };
+
+  auto allocateClaimed = [&](size_t idx, bool advanceFrontier) {
+    ArrayRef<CGNode *> users = resourceToUsers[idx];
+    uint64_t start = alignTo(
+        getMaxKernelFrontier(users, kernelFrontiers, traits.initialValue()),
+        traits.alignment(resources[idx]));
+    assignResource(idx, start);
+    if (!advanceFrontier)
+      return;
+
+    uint64_t next = start + traits.size(resources[idx]);
+    traits.printAllocation(resources[idx], start, users.size());
+    for (CGNode *user : users)
+      kernelFrontiers[user] = next;
+  };
+
+  for (size_t idx : claimedNonZero)
+    allocateClaimed(idx, /*advanceFrontier=*/true);
+
+  for (size_t idx : claimedZeroSize)
+    allocateClaimed(idx, /*advanceFrontier=*/false);
+
+  // Resources not reachable from any kernel still need valid addresses for
+  // relocation resolution. Assign them independently from the initial frontier
+  // because no kernel's resource total depends on them.
+  if (!unclaimedNonZero.empty() || !unclaimedZeroSize.empty()) {
+    llvm::sort(unclaimedNonZero, idxCmp);
+    llvm::sort(unclaimedZeroSize, idxCmp);
+    uint64_t frontier = traits.initialValue();
+    for (size_t idx : unclaimedNonZero) {
+      frontier = alignTo(frontier, traits.alignment(resources[idx]));
+      assignResource(idx, frontier);
+      frontier += traits.size(resources[idx]);
+    }
+    if (!unclaimedZeroSize.empty()) {
+      Align maxZeroAlign(1);
+      for (size_t idx : unclaimedZeroSize)
+        maxZeroAlign = std::max(maxZeroAlign, traits.alignment(resources[idx]));
+      uint64_t zeroBase = alignTo(frontier, maxZeroAlign);
+      for (size_t idx : unclaimedZeroSize)
+        assignResource(idx, zeroBase);
+    }
+  }
+
+#ifndef NDEBUG
+  assertResourceLayoutInvariants(ArrayRef<ResourceT>(resources), kernels,
+                                 resourceToUsers, assigned, traits);
+#endif
+}
+
+//===----------------------------------------------------------------------===//
+// Section parsing
+//===----------------------------------------------------------------------===//
+
+// Build relocation map: byte offset -> {ELF symbol index, addend}.
+// The addend is needed to resolve STT_SECTION symbols: ELF assemblers
+// canonically convert relocations for local symbols in their own sections
+// into section_symbol + addend form, so we must track the addend to map
+// back to the actual function symbol (see resolveSecSymbol).
+// Extract explicit RELA/CREL addends and treat REL entries as addend zero.
+template <class RelTy> static int64_t getAddend(const RelTy &rel) {
+  if constexpr (RelTy::HasAddend)
+    return rel.r_addend;
+  return 0;
+}
+
+// Build the relocation lookup for one .amdgpu.info section.
+template <class ELFT>
+static DenseMap<uint64_t, RelocInfo> buildRelocMap(ObjFile<ELFT> *obj,
+                                                   uint32_t secIndex) {
+  ArrayRef<typename ELFT::Shdr> objSections = obj->template getELFShdrs<ELFT>();
+  const ELFFile<ELFT> &elfObj = obj->getObj();
+  DenseMap<uint64_t, RelocInfo> relocMap;
+  for (size_t i = 0, e = objSections.size(); i < e; ++i) {
+    const auto &relSec = objSections[i];
+    if (relSec.sh_info != secIndex)
+      continue;
+    if (relSec.sh_type == SHT_RELA) {
+      for (const auto &rel :
+           CHECK(elfObj.relas(relSec), "could not read rela section"))
+        relocMap[rel.r_offset] = {rel.getSymbol(false), getAddend(rel)};
+      break;
+    }
+    if (relSec.sh_type == SHT_REL) {
+      for (const auto &rel :
+           CHECK(elfObj.rels(relSec), "could not read rel section"))
+        relocMap[rel.r_offset] = {rel.getSymbol(false), 0};
+      break;
+    }
+    if (relSec.sh_type == SHT_CREL) {
+      auto crels = CHECK(elfObj.crels(relSec), "could not read crel section");
+      for (const auto &rel : crels.first)
+        relocMap[rel.r_offset] = {rel.getSymbol(false), getAddend(rel)};
+      for (const auto &rel : crels.second)
+        relocMap[rel.r_offset] = {rel.getSymbol(false), getAddend(rel)};
+      break;
+    }
+  }
+  return relocMap;
+}
+
+// Map function definition addresses back to symbols so section-symbol
+// relocations can recover the original named function.
+template <class ELFT>
+static DenseMap<std::pair<SectionBase *, uint64_t>, Symbol *>
+buildSymbolAddressMap(ObjFile<ELFT> *obj) {
+  DenseMap<std::pair<SectionBase *, uint64_t>, Symbol *> addrToSym;
+  for (Symbol *sym : obj->getSymbols()) {
+    if (!sym || sym->getName().empty())
+      continue;
+    auto *def = dyn_cast<Defined>(sym);
+    if (!def || !def->section)
+      continue;
+    addrToSym.try_emplace({def->section, def->value}, sym);
+  }
+  return addrToSym;
+}
+
+// Resolve an STT_SECTION symbol + addend to the named function symbol at that
+// address. ELF assemblers replace relocations targeting local symbols with
+// section_symbol + addend (standard ELF canonicalization). Since section
+// symbols have empty names in LLD, the .amdgpu.info parser cannot identify
+// the function from the section symbol alone. Use a per-object address map to
+// find the named Defined symbol at the matching section and offset.
+static Symbol *resolveSecSymbol(
+    Symbol &secSym, int64_t addend,
+    const DenseMap<std::pair<SectionBase *, uint64_t>, Symbol *> &addrToSym) {
+  auto *secDef = dyn_cast<Defined>(&secSym);
+  if (!secDef || !secDef->section)
+    return nullptr;
+  auto it = addrToSym.find({secDef->section, static_cast<uint64_t>(addend)});
+  return it == addrToSym.end() ? nullptr : it->second;
+}
+
+using namespace llvm::AMDGPU;
+
+// Resolve a relocation attached to a .amdgpu.info payload field. The result is
+// either the referenced symbol or the named function represented by a
+// section-symbol relocation.
+template <class ELFT>
+static Symbol *resolveRelocSym(
+    ObjFile<ELFT> *obj, const DenseMap<uint64_t, RelocInfo> &relocMap,
+    const DenseMap<std::pair<SectionBase *, uint64_t>, Symbol *> &addrToSym,
+    uint64_t off, StringRef diagName) {
+  auto it = relocMap.find(off);
+  if (it == relocMap.end())
+    return nullptr;
+  Symbol *sym = &obj->getSymbol(it->second.symIdx);
+  if (sym->getName().empty()) {
+    sym = resolveSecSymbol(*sym, it->second.addend, addrToSym);
+    if (!sym) {
+      Warn(obj->ctx) << obj->getName() << ": .amdgpu.info " << diagName
+                     << " at offset " << off
+                     << " references a section symbol that could not be "
+                        "resolved to a named function symbol";
+    }
+  }
+  return sym;
+}
+
+// Return a string from .amdgpu.strtab without reading past the section if
+// malformed input omits the trailing NUL.
+static StringRef getInfoString(StringRef strtab, uint32_t offset) {
+  if (offset >= strtab.size())
+    return StringRef();
+  StringRef tail = strtab.substr(offset);
+  size_t nul = tail.find('\0');
+  return tail.substr(0, nul);
+}
+
+// .amdgpu.info is emitted as a standalone section, so records from losing weak
+// definitions or discarded COMDAT groups can survive. Only the prevailing
+// definition may contribute local resources and edges. Non-prevailing scopes
+// may still contribute type IDs for address-taken declarations.
+static bool isPrevailingInfoFunction(ELFFileBase *obj, Symbol *sym) {
+  auto *def = dyn_cast<Defined>(sym);
+  if (!def || !def->section || def->section == &InputSection::discarded)
+    return false;
+  return def->section->file == obj;
+}
+
+// Check whether this object participates in AMDGPU object-linking metadata.
+template <class ELFT> static bool hasAMDGPUInfoSection(ObjFile<ELFT> *obj) {
+  return obj->amdgpuInfoSectionIndex != 0;
+}
+
+// Parse one object's .amdgpu.info and .amdgpu.strtab sections into the shared
+// call graph, direct resource records, and direct LDS/barrier uses.
+template <class ELFT>
+static void parseInfoSection(ObjFile<ELFT> *obj, AMDGPUCallGraph &cg,
+                             const DenseMap<Symbol *, size_t> &ldsSymToIndex,
+                             const DenseMap<Symbol *, size_t> &barSymToIndex) {
+  if (!hasAMDGPUInfoSection(obj))
+    return;
+
+  ArrayRef<typename ELFT::Shdr> objSections = obj->template getELFShdrs<ELFT>();
+  const auto &sec = objSections[obj->amdgpuInfoSectionIndex];
+  ArrayRef<uint8_t> data = CHECK(obj->getObj().getSectionContents(sec),
+                                 "could not read .amdgpu.info section");
+  if (data.empty())
+    return;
+
+  DenseMap<uint64_t, RelocInfo> relocMap =
+      buildRelocMap(obj, obj->amdgpuInfoSectionIndex);
+  DenseMap<std::pair<SectionBase *, uint64_t>, Symbol *> addrToSym =
+      buildSymbolAddressMap(obj);
+
+  StringRef strtab;
+  if (obj->amdgpuStrtabSectionIndex != 0) {
+    const auto &strtabSec = objSections[obj->amdgpuStrtabSectionIndex];
+    ArrayRef<uint8_t> strtabData =
+        CHECK(obj->getObj().getSectionContents(strtabSec),
+              "could not read .amdgpu.strtab section");
+    strtab = StringRef(reinterpret_cast<const char *>(strtabData.data()),
+                       strtabData.size());
+  }
+
+  CGNode *curNode = nullptr;
+  bool curFuncIsPrevailing = false;
+  size_t pos = 0;
+  while (pos + 2 <= data.size()) {
+    uint8_t kind = data[pos];
+    uint8_t len = data[pos + 1];
+    size_t payloadOff = pos + 2;
+    if (payloadOff + len > data.size())
+      break;
+
+    switch (kind) {
+    case static_cast<uint8_t>(AMDGPU::InfoKind::INFO_FUNC): {
+      if (len < 8)
+        break;
+      Symbol *sym =
+          resolveRelocSym(obj, relocMap, addrToSym, payloadOff, "func");
+      if (!sym || sym->getName().empty()) {
+        curNode = nullptr;
+        curFuncIsPrevailing = false;
+        break;
+      }
+      curNode = &cg.getOrCreate(sym);
+      curFuncIsPrevailing = isPrevailingInfoFunction(obj, sym);
+      break;
+    }
+    case static_cast<uint8_t>(AMDGPU::InfoKind::INFO_FLAGS): {
+      if (!curNode || !curFuncIsPrevailing || len < 4)
+        break;
+      AMDGPU::FuncInfoFlags flags = static_cast<AMDGPU::FuncInfoFlags>(
+          read32le(data.data() + payloadOff));
+      curNode->localRes.usesVCC =
+          !!(flags & AMDGPU::FuncInfoFlags::FUNC_USES_VCC);
+      curNode->localRes.usesFlatScratch =
+          !!(flags & AMDGPU::FuncInfoFlags::FUNC_USES_FLAT_SCRATCH);
+      curNode->localRes.hasDynSizedStack =
+          !!(flags & AMDGPU::FuncInfoFlags::FUNC_HAS_DYN_STACK);
+      curNode->hasLocalRes = true;
+      break;
+    }
+    case static_cast<uint8_t>(AMDGPU::InfoKind::INFO_NUM_VGPR): {
+      if (!curNode || !curFuncIsPrevailing || len < 4)
+        break;
+      curNode->localRes.numArchVGPR = read32le(data.data() + payloadOff);
+      curNode->hasLocalRes = true;
+      break;
+    }
+    case static_cast<uint8_t>(AMDGPU::InfoKind::INFO_NUM_AGPR): {
+      if (!curNode || !curFuncIsPrevailing || len < 4)
+        break;
+      curNode->localRes.numAccVGPR = read32le(data.data() + payloadOff);
+      curNode->hasLocalRes = true;
+      break;
+    }
+    case static_cast<uint8_t>(AMDGPU::InfoKind::INFO_NUM_SGPR): {
+      if (!curNode || !curFuncIsPrevailing || len < 4)
+        break;
+      curNode->localRes.numSGPR = read32le(data.data() + payloadOff);
+      curNode->hasLocalRes = true;
+      break;
+    }
+    case static_cast<uint8_t>(AMDGPU::InfoKind::INFO_PRIVATE_SEGMENT_SIZE): {
+      if (!curNode || !curFuncIsPrevailing || len < 4)
+        break;
+      curNode->localRes.scratchSize = read32le(data.data() + payloadOff);
+      curNode->hasLocalRes = true;
+      break;
+    }
+    case static_cast<uint8_t>(AMDGPU::InfoKind::INFO_OCCUPANCY): {
+      if (!curNode || !curFuncIsPrevailing || len < 4)
+        break;
+      curNode->localRes.occupancy = read32le(data.data() + payloadOff);
+      curNode->hasLocalRes = true;
+      break;
+    }
+    case static_cast<uint8_t>(AMDGPU::InfoKind::INFO_WAVE_SIZE): {
+      if (!curNode || !curFuncIsPrevailing || len < 4)
+        break;
+      curNode->localRes.waveSize = read32le(data.data() + payloadOff);
+      curNode->hasLocalRes = true;
+      break;
+    }
+    case static_cast<uint8_t>(AMDGPU::InfoKind::INFO_USE): {
+      if (!curNode || !curFuncIsPrevailing || len < 8)
+        break;
+      Symbol *resSym =
+          resolveRelocSym(obj, relocMap, addrToSym, payloadOff, "use");
+      if (!resSym || resSym->getName().empty())
+        break;
+      auto barIt = barSymToIndex.find(resSym);
+      if (barIt != barSymToIndex.end()) {
+        LLVM_DEBUG(dbgs() << "  barrier use: " << curNode->sym->getName()
+                          << " -> " << resSym->getName() << "\n");
+        curNode->barrierUseIndices.push_back(barIt->second);
+        cg.setHasBarrierUses();
+        break;
+      }
+      auto ldsIt = ldsSymToIndex.find(resSym);
+      if (ldsIt != ldsSymToIndex.end()) {
+        LLVM_DEBUG(dbgs() << "  use: " << curNode->sym->getName() << " -> "
+                          << resSym->getName() << "\n");
+        curNode->ldsUseIndices.push_back(ldsIt->second);
+        cg.setHasLDSUses();
+      }
+      break;
+    }
+    case static_cast<uint8_t>(AMDGPU::InfoKind::INFO_CALL): {
+      if (!curNode || !curFuncIsPrevailing || len < 8)
+        break;
+      Symbol *dstSym =
+          resolveRelocSym(obj, relocMap, addrToSym, payloadOff, "call");
+      if (!dstSym || dstSym->getName().empty())
+        break;
+      CGNode &dst = cg.getOrCreate(dstSym);
+      LLVM_DEBUG(dbgs() << "  call: " << curNode->sym->getName() << " -> "
+                        << dstSym->getName() << "\n");
+      curNode->callees.push_back(&dst);
+      break;
+    }
+    case static_cast<uint8_t>(AMDGPU::InfoKind::INFO_INDIRECT_CALL): {
+      if (!curNode || !curFuncIsPrevailing || len < 4)
+        break;
+      uint32_t typeIdOff = read32le(data.data() + payloadOff);
+      StringRef typeId = getInfoString(strtab, typeIdOff);
+      if (!typeId.empty()) {
+        LLVM_DEBUG(dbgs() << "  indirect-call: " << curNode->sym->getName()
+                          << " enc=" << typeId << "\n");
+        cg.addIndirectCall(curNode, typeId);
+      }
+      break;
+    }
+    case static_cast<uint8_t>(AMDGPU::InfoKind::INFO_TYPEID): {
+      if (!curNode || len < 4)
+        break;
+      cg.markAddressTaken(curNode);
+      uint32_t typeIdOff = read32le(data.data() + payloadOff);
+      StringRef typeId = getInfoString(strtab, typeIdOff);
+      if (!typeId.empty()) {
+        LLVM_DEBUG(dbgs() << "  signature: " << curNode->sym->getName()
+                          << " typeId=" << typeId << "\n");
+        cg.addSignature(curNode, typeId);
+      }
+      break;
+    }
+    default:
+      LLVM_DEBUG(dbgs() << "  unknown info kind " << (unsigned)kind
+                        << " (len=" << (unsigned)len << "), skipping\n");
+      break;
+    }
+
+    pos = payloadOff + len;
+  }
+
+  // Emit debug summary of parsed resources.
+  if (curNode && curNode->hasLocalRes) {
+    LLVM_DEBUG({
+      for (auto &[sym, node] : cg) {
+        if (!node->hasLocalRes)
+          continue;
+        dbgs() << "  resource: " << sym->getName()
+               << " vgpr=" << node->localRes.numArchVGPR
+               << " agpr=" << node->localRes.numAccVGPR
+               << " sgpr=" << node->localRes.numSGPR
+               << " scratch=" << node->localRes.scratchSize
+               << " occupancy=" << node->localRes.occupancy << "\n";
+      }
+    });
+  }
+}
+
+// Kernels are identified by the companion kernel descriptor symbol emitted as
+// <kernel>.kd.
+static bool hasKernelDescriptor(Ctx &ctx, Symbol *sym) {
+  std::string kdName = (sym->getName() + ".kd").str();
+  Symbol *kdSym = ctx.symtab->find(kdName);
+  Defined *kdDef = dyn_cast_or_null<Defined>(kdSym);
+  return kdDef && kdDef->section;
+}
+
+// Mark graph nodes with kernel descriptors as roots for reachability and
+// resource propagation.
+static void markKernelsWithDescriptors(Ctx &ctx, AMDGPUCallGraph &cg) {
+  for (auto &[sym, node] : cg) {
+    if (hasKernelDescriptor(ctx, sym)) {
+      LLVM_DEBUG(dbgs() << "  kernel: " << sym->getName() << "\n");
+      cg.addKernel(*node);
+    }
+  }
+}
+
+//===----------------------------------------------------------------------===//
+// Call graph SCC condensation, reachability, and resource propagation
+//===----------------------------------------------------------------------===//
+
+// Validate local resource metadata for a kernel-reachable node before resource
+// propagation depends on it.
+static bool validateResourceNode(Ctx &ctx, CGNode *node) {
+  assert(node->hasLocalRes && "missing resource usage after alias resolution");
+
+  if (node->localRes.occupancy == 0) {
+    Err(ctx) << "AMDGPU: function '" << node->sym->getName()
+             << "' has invalid ABI occupancy 0";
+    LLVM_DEBUG(dbgs() << "    resolve " << node->sym->getName()
+                      << " (invalid occupancy)\n");
+    return false;
+  }
+
+  return true;
+}
+
+// Merge resource usage from another ResourceInfo into the result using
+// element-wise max for register/scratch counts and OR for boolean flags.
+static void mergeResourceInfo(ResourceInfo &result, const ResourceInfo &info) {
+  result.numArchVGPR = std::max(result.numArchVGPR, info.numArchVGPR);
+  result.numAccVGPR = std::max(result.numAccVGPR, info.numAccVGPR);
+  result.numSGPR = std::max(result.numSGPR, info.numSGPR);
+  result.scratchSize = std::max(result.scratchSize, info.scratchSize);
+  result.usesVCC |= info.usesVCC;
+  result.usesFlatScratch |= info.usesFlatScratch;
+  result.hasDynSizedStack |= info.hasDynSizedStack;
+}
+
+bool AMDGPUCallGraph::buildCondensedGraph(Ctx &ctx, bool validateResources) {
+  DenseMap<CGNode *, unsigned> nodeIndex;
+  DenseMap<CGNode *, unsigned> lowLink;
+  DenseSet<CGNode *> onStack;
+  SmallVector<CGNode *, 16> stack;
+  unsigned nextIndex = 0;
+
+  // Start from kernels so resource diagnostics are limited to code that can
+  // execute. A validation failure aborts the build; callers do not inspect the
+  // partially constructed SCC state.
+  for (CGNode *kernel : kernels()) {
+    if (nodeIndex.count(kernel))
+      continue;
+    if (!buildSCCs(ctx, kernel, nodeIndex, lowLink, onStack, stack, nextIndex,
+                   validateResources))
+      return false;
+  }
+
+  buildSCCEdges();
+  return true;
+}
+
+bool AMDGPUCallGraph::buildSCCs(Ctx &ctx, CGNode *node,
+                                DenseMap<CGNode *, unsigned> &nodeIndex,
+                                DenseMap<CGNode *, unsigned> &lowLink,
+                                DenseSet<CGNode *> &onStack,
+                                SmallVectorImpl<CGNode *> &stack,
+                                unsigned &nextIndex, bool validateResources) {
+  if (validateResources && !validateResourceNode(ctx, node))
+    return false;
+
+  nodeIndex[node] = nextIndex;
+  lowLink[node] = nextIndex;
+  ++nextIndex;
+  stack.push_back(node);
+  onStack.insert(node);
+
+  if (validateResources) {
+    const ResourceInfo &info = node->localRes;
+    LLVM_DEBUG(dbgs() << "    resolve " << node->sym->getName()
+                      << " (has local res: vgpr=" << info.numArchVGPR
+                      << " sgpr=" << info.numSGPR
+                      << " scratch=" << info.scratchSize << ")\n");
+  }
+
+  if (!node->callees.empty()) {
+    LLVM_DEBUG({
+      if (validateResources) {
+        dbgs() << "    " << node->sym->getName() << " has "
+               << node->callees.size() << " callees:";
+        for (CGNode *c : node->callees)
+          dbgs() << " " << c->sym->getName();
+        dbgs() << "\n";
+      }
+    });
+
+    for (CGNode *callee : node->callees) {
+      if (validateResources) {
+        if (!validateResourceNode(ctx, callee))
+          return false;
+
+        if (callee->localRes.occupancy < node->localRes.occupancy) {
+          Err(ctx) << "AMDGPU: incompatible ABI occupancy: function '"
+                   << node->sym->getName() << "' requires occupancy "
+                   << node->localRes.occupancy << " but calls '"
+                   << callee->sym->getName() << "' with occupancy "
+                   << callee->localRes.occupancy;
+          return false;
+        }
+      }
+
+      auto calleeIndex = nodeIndex.find(callee);
+      if (calleeIndex == nodeIndex.end()) {
+        if (!buildSCCs(ctx, callee, nodeIndex, lowLink, onStack, stack,
+                       nextIndex, validateResources))
+          return false;
+        lowLink[node] = std::min(lowLink[node], lowLink[callee]);
+      } else if (onStack.count(callee)) {
+        lowLink[node] = std::min(lowLink[node], calleeIndex->second);
+      }
+    }
+  }
+
+  if (lowLink[node] != nodeIndex[node])
+    return true;
+
+  unsigned sccId = sccs.size();
+  sccs.emplace_back();
+  CallGraphSCC &scc = sccs.back();
+
+  for (;;) {
+    CGNode *member = stack.pop_back_val();
+    onStack.erase(member);
+    scc.members.push_back(member);
+    nodeToSCC[member] = sccId;
+
+    if (validateResources)
+      mergeResourceInfo(scc.localRes, member->localRes);
+
+    for (size_t idx : member->ldsUseIndices)
+      scc.reachableLDS.insert(idx);
+
+    for (size_t idx : member->barrierUseIndices)
+      scc.reachableBarriers.insert(idx);
+
+    if (member == node)
+      break;
+  }
+
+  return true;
+}
+
+void AMDGPUCallGraph::buildSCCEdges() {
+  for (unsigned sccId = 0, e = sccs.size(); sccId != e; ++sccId) {
+    CallGraphSCC &scc = sccs[sccId];
+    if (scc.members.size() > 1) {
+      scc.isRecursive = true;
+      scc.localRes.hasDynSizedStack = true;
+    }
+
+    DenseSet<unsigned> seenCallees;
+    for (CGNode *member : scc.members) {
+      for (CGNode *callee : member->callees) {
+        auto it = nodeToSCC.find(callee);
+        assert(it != nodeToSCC.end() && "callee should be in call graph SCC");
+
+        unsigned calleeSCC = it->second;
+        if (calleeSCC == sccId) {
+          scc.isRecursive = true;
+          scc.localRes.hasDynSizedStack = true;
+          continue;
+        }
+
+        assert(calleeSCC < sccId &&
+               "sccs should be in reverse topological order");
+
+        if (seenCallees.insert(calleeSCC).second)
+          scc.callees.push_back(calleeSCC);
+      }
+    }
+  }
+}
+
+//===----------------------------------------------------------------------===//
+// LDS resolution
+//===----------------------------------------------------------------------===//
+
+void AMDGPUCallGraph::computeKernelReachability() {
+  for (unsigned sccId = 0, e = sccs.size(); sccId != e; ++sccId) {
+    CallGraphSCC &scc = sccs[sccId];
+    for (unsigned calleeSCC : scc.callees) {
+      assert(calleeSCC < sccId &&
+             "sccs should be in reverse topological order");
+      const CallGraphSCC &callee = sccs[calleeSCC];
+      scc.reachableLDS.insert(callee.reachableLDS.begin(),
+                              callee.reachableLDS.end());
+      scc.reachableBarriers.insert(callee.reachableBarriers.begin(),
+                                   callee.reachableBarriers.end());
+    }
+  }
+
+  for (CGNode *kernel : kernels()) {
+    auto it = nodeToSCC.find(kernel);
+    assert(it != nodeToSCC.end() && "kernel should be in call graph SCC");
+    const CallGraphSCC &scc = sccs[it->second];
+    kernel->reachableLDS.clear();
+    kernel->reachableLDS.insert(scc.reachableLDS.begin(),
+                                scc.reachableLDS.end());
+    kernel->reachableBarriers.clear();
+    kernel->reachableBarriers.insert(scc.reachableBarriers.begin(),
+                                     scc.reachableBarriers.end());
+  }
+}
+
+// Compute each kernel's fixed LDS usage from the final offsets of the LDS
+// objects reachable from that kernel.
+static void computeKernelLDSSizes(ArrayRef<LDSSymbolInfo> ldsSymbols,
+                                  AMDGPUCallGraph &cg) {
+  if (!cg.hasLDSUses()) {
+    uint32_t totalSize = 0;
+    for (const LDSSymbolInfo &lds : ldsSymbols)
+      totalSize = std::max<uint64_t>(totalSize, lds.assignedOffset + lds.size);
+    for (CGNode *kernel : cg.kernels())
+      kernel->ldsSize = totalSize;
+    return;
+  }
+
+  for (CGNode *kernel : cg.kernels()) {
+    uint32_t maxEnd = 0;
+    for (size_t idx : kernel->reachableLDS)
+      maxEnd = std::max<uint64_t>(maxEnd, ldsSymbols[idx].assignedOffset +
+                                              ldsSymbols[idx].size);
+    kernel->ldsSize = maxEnd;
+  }
+}
+
+// Assign LDS offsets, rewrite LDS symbols to those offsets, and record each
+// kernel's group segment size.
+static void resolveLDS(Ctx &ctx, SmallVectorImpl<LDSSymbolInfo> &ldsSymbols,
+                       AMDGPUCallGraph &cg) {
+  LLVM_DEBUG(dbgs() << "AMDGPU LDS: assigning grouped offsets\n");
+  assignGroupedResources(ldsSymbols, cg, LDSAllocationTraits());
+  computeKernelLDSSizes(ldsSymbols, cg);
+
+  // Symbol::overwrite preserves the old symbol's visibility, so for shared-
+  // object links (AMDGPU code objects use -shared) we must explicitly force
+  // STV_HIDDEN to prevent the symbol from being preemptible, which would cause
+  // R_AMDGPU_ABS32_LO relocations to be rejected by the relocation scanner.
+  LLVM_DEBUG(dbgs() << "AMDGPU LDS: final symbol assignments:\n");
+  for (const LDSSymbolInfo &lds : ldsSymbols) {
+    LLVM_DEBUG(dbgs() << "  " << lds.sym->getName() << " -> offset="
+                      << lds.assignedOffset << " size=" << lds.size << "\n");
+    Defined(ctx, ctx.internalFile, lds.sym->getName(), STB_GLOBAL, STV_HIDDEN,
+            STT_NOTYPE, lds.assignedOffset, lds.size, nullptr)
+        .overwrite(*lds.sym);
+    if (ctx.arg.shared)
+      lds.sym->stOther = (lds.sym->stOther & ~3) | STV_HIDDEN;
+  }
+
+  LLVM_DEBUG({
+    dbgs() << "AMDGPU LDS: per-kernel LDS sizes:\n";
+    for (CGNode *kernel : cg.kernels())
+      dbgs() << "  " << kernel->sym->getName() << " -> " << kernel->ldsSize
+             << " bytes\n";
+  });
+}
+
+//===----------------------------------------------------------------------===//
+// Named barrier resolution
+//===----------------------------------------------------------------------===//
+
+static uint32_t computeMaxNamedBarrierEnd(ArrayRef<NamedBarrierInfo> barriers) {
+  uint32_t maxBarEnd = 0;
+  for (const NamedBarrierInfo &bar : barriers) {
+    if (bar.slotCount == 0)
+      continue;
+    maxBarEnd = std::max(maxBarEnd, bar.assignedBarId + bar.slotCount - 1);
+  }
+  return maxBarEnd;
+}
+
+static void computeKernelNamedBarrierCounts(ArrayRef<NamedBarrierInfo> barriers,
+                                            AMDGPUCallGraph &cg) {
+  if (!cg.hasBarrierUses()) {
+    uint32_t totalCount = computeMaxNamedBarrierEnd(barriers);
+    for (CGNode *kernel : cg.kernels())
+      kernel->numNamedBarrier = totalCount;
+    return;
+  }
+
+  for (CGNode *kernel : cg.kernels()) {
+    uint32_t maxBarEnd = 0;
+    for (size_t idx : kernel->reachableBarriers) {
+      if (barriers[idx].slotCount == 0)
+        continue;
+      uint32_t end = barriers[idx].assignedBarId + barriers[idx].slotCount - 1;
+      maxBarEnd = std::max(maxBarEnd, end);
+    }
+    kernel->numNamedBarrier = maxBarEnd;
+  }
+}
+
+static void checkNamedBarrierCounts(Ctx &ctx, AMDGPUCallGraph &cg) {
+  for (CGNode *kernel : cg.kernels()) {
+    if (kernel->numNamedBarrier <= 31)
+      continue;
+    Err(ctx) << "AMDGPU: named barrier ID overflow (max ID "
+             << kernel->numNamedBarrier << " exceeds limit of 31) in kernel '"
+             << kernel->sym->getName() << "'";
+  }
+}
+
+// Assign named-barrier hardware IDs, rewrite named-barrier symbols to encoded
+// barrier addresses, and record each kernel's named-barrier count.
+static void resolveNamedBarriers(Ctx &ctx,
+                                 SmallVectorImpl<NamedBarrierInfo> &barriers,
+                                 AMDGPUCallGraph &cg) {
+  LLVM_DEBUG(dbgs() << "AMDGPU Named Barriers: assigning grouped IDs\n");
+  assignGroupedResources(barriers, cg, NamedBarrierAllocationTraits());
+  computeKernelNamedBarrierCounts(barriers, cg);
+  checkNamedBarrierCounts(ctx, cg);
+
+  constexpr uint32_t barScope = 0; // BARRIER_SCOPE_WORKGROUP
+  LLVM_DEBUG(dbgs() << "AMDGPU Named Barriers: final assignments:\n");
+  for (NamedBarrierInfo &bar : barriers) {
+    if (bar.assignedBarId == 0)
+      continue;
+    uint32_t addr = 0x802000u | (barScope << 9) | (bar.assignedBarId << 4);
+    LLVM_DEBUG(dbgs() << "  " << bar.sym->getName()
+                      << " -> barId=" << bar.assignedBarId << " addr=0x"
+                      << Twine::utohexstr(addr) << "\n");
+    Defined(ctx, ctx.internalFile, bar.sym->getName(), STB_GLOBAL, STV_HIDDEN,
+            STT_NOTYPE, addr, 0, nullptr)
+        .overwrite(*bar.sym);
+    if (ctx.arg.shared)
+      bar.sym->stOther = (bar.sym->stOther & ~3) | STV_HIDDEN;
+  }
+
+  for (CGNode *kernel : cg.kernels()) {
+    LLVM_DEBUG(dbgs() << "  " << kernel->sym->getName() << " numNamedBarrier="
+                      << kernel->numNamedBarrier << "\n");
+  }
+}
+
+// Check that each kernel's final LDS usage satisfies the ABI occupancy recorded
+// in .amdgpu.info.
+static bool validateKernelLDSOccupancy(Ctx &ctx, AMDGPUCallGraph &cg,
+                                       const MCSubtargetInfo &sti) {
+  for (CGNode *kernel : cg.kernels()) {
+    assert(kernel->hasLocalRes &&
+           "kernel resource usage should have been validated");
+    assert(kernel->localRes.occupancy != 0 &&
+           "kernel occupancy should have been validated");
+    if (AMDGPU::IsaInfo::isLocalMemorySizeCompatibleWithOccupancy(
+            sti, kernel->ldsSize, kernel->localRes.occupancy))
+      continue;
+
+    Err(ctx) << "AMDGPU: kernel '" << kernel->sym->getName() << "' uses "
+             << kernel->ldsSize << " bytes of LDS, which does not meet ABI "
+             << "occupancy " << kernel->localRes.occupancy;
+    return false;
+  }
+
+  return true;
+}
+
+//===----------------------------------------------------------------------===//
+// Resource usage propagation
+//===----------------------------------------------------------------------===//
+
+void AMDGPUCallGraph::propagateResourceUsage() {
+  for (unsigned sccId = 0, e = sccs.size(); sccId != e; ++sccId) {
+    CallGraphSCC &scc = sccs[sccId];
+    ResourceInfo result = scc.localRes;
+    uint32_t maxCalleeScratch = 0;
+    for (unsigned calleeSCC : scc.callees) {
+      assert(calleeSCC < sccId &&
+             "sccs should be in reverse topological order");
+      const CallGraphSCC &callee = sccs[calleeSCC];
+      assert(callee.hasPropagatedRes && "callee resource should be propagated");
+      mergeResourceInfo(result, callee.propagatedRes);
+      maxCalleeScratch =
+          std::max(maxCalleeScratch, callee.propagatedRes.scratchSize);
+    }
+    // For recursive SCCs, scratchSize remains the finite fixed-frame
+    // contribution. The unbounded recursive depth is represented by
+    // hasDynSizedStack, which forces dynamic stack handling.
+    result.scratchSize = scc.localRes.scratchSize + maxCalleeScratch;
+
+    scc.propagatedRes = result;
+    scc.hasPropagatedRes = true;
+    for (CGNode *member : scc.members) {
+      member->propagatedRes = result;
+      member->hasPropagatedRes = true;
+    }
+  }
+
+  for (CGNode *kernel : kernels()) {
+    LLVM_DEBUG(dbgs() << "  propagated " << kernel->sym->getName()
+                      << ": vgpr=" << kernel->propagatedRes.numArchVGPR
+                      << " agpr=" << kernel->propagatedRes.numAccVGPR
+                      << " sgpr=" << kernel->propagatedRes.numSGPR
+                      << " scratch=" << kernel->propagatedRes.scratchSize
+                      << " dynstack=" << kernel->propagatedRes.hasDynSizedStack
+                      << "\n");
+  }
+}
+
+// Resolve all resource fields that are independent of the existing descriptor
+// contents. VGPR block encoding is left to the KD patcher because it depends on
+// the per-descriptor ENABLE_WAVEFRONT_SIZE32 bit.
+static ResolvedKernelResources
+resolveKernelResources(const CGNode &kernel, const MCSubtargetInfo &sti) {
+  assert(kernel.hasPropagatedRes &&
+         "kernel resource usage should have been propagated");
+
+  const ResourceInfo &info = kernel.propagatedRes;
+  ResolvedKernelResources res;
+  res.totalVGPR = AMDGPU::getTotalNumVGPRs(AMDGPU::isGFX90A(sti),
+                                           info.numAccVGPR, info.numArchVGPR);
+  res.totalSGPR = info.numSGPR + AMDGPU::IsaInfo::getNumExtraSGPRs(
+                                     sti, info.usesVCC, info.usesFlatScratch);
+  res.numAccVGPR = info.numAccVGPR;
+  res.scratchSize = info.scratchSize;
+  unsigned archGranule = AMDGPU::IsaInfo::getArchVGPRAllocGranule();
+  res.accumOffset = divideCeil(std::max(info.numArchVGPR, 1u), archGranule) - 1;
+  res.namedBarCnt = divideCeil(kernel.numNamedBarrier, 4);
+  res.scratchEnable = res.scratchSize > 0 || info.hasDynSizedStack;
+  res.usesDynamicStack = info.hasDynSizedStack;
+  return res;
+}
+
+// Patch kernel descriptors in-place with resolved LDS, named-barrier, and
+// propagated resource fields.
+static void patchKernelDescriptors(Ctx &ctx, AMDGPUCallGraph &cg,
+                                   const MCSubtargetInfo *sti, bool hasLDS,
+                                   bool hasBarriers) {
+  using namespace llvm::amdhsa;
+  bool hasAccumOffset = AMDGPU::isGFX90A(*sti);
+  bool sgprBlocksAlwaysZero = AMDGPU::isGFX10Plus(*sti);
+  bool hasNamedBarCnt = AMDGPU::isGFX1250Plus(*sti);
+
+  // Track sections that have been copied to writable memory so we don't
+  // allocate redundant copies when multiple KDs share the same section.
+  DenseSet<InputSection *> copiedSections;
+
+  for (CGNode *kernel : cg.kernels()) {
+    StringRef name = kernel->sym->getName();
+    std::string kdName = (name + ".kd").str();
+    Symbol *kdSym = ctx.symtab->find(kdName);
+    if (!kdSym)
+      continue;
+    auto *kdDef = dyn_cast<Defined>(kdSym);
+    if (!kdDef || !kdDef->section)
+      continue;
+    auto *isec = dyn_cast<InputSection>(kdDef->section);
+    if (!isec)
+      continue;
+
+    uint64_t off = kdDef->value;
+    if (off + sizeof(kernel_descriptor_t) > isec->size)
+      continue;
+
+    // The section content may be in read-only mmap'd memory. Make a writable
+    // copy the first time we need to patch a KD in this section.
+    if (copiedSections.insert(isec).second) {
+      auto *newBuf = ctx.bAlloc.Allocate<uint8_t>(isec->size);
+      memcpy(newBuf, isec->content_, isec->size);
+      isec->content_ = newBuf;
+    }
+
+    auto *buf = const_cast<uint8_t *>(isec->content_) + off;
+
+    if (hasLDS) {
+      write32le(buf + GROUP_SEGMENT_FIXED_SIZE_OFFSET, kernel->ldsSize);
+      LLVM_DEBUG(dbgs() << "  patched " << name
+                        << ".kd group_segment_fixed_size = " << kernel->ldsSize
+                        << "\n");
+    }
+
+    if (!kernel->hasPropagatedRes)
+      continue;
+
+    ResolvedKernelResources res = resolveKernelResources(*kernel, *sti);
+    write32le(buf + PRIVATE_SEGMENT_FIXED_SIZE_OFFSET, res.scratchSize);
+
+    // Read the per-kernel ENABLE_WAVEFRONT_SIZE32 bit from the KD -- it
+    // affects the VGPR encoding granule on GFX10+.
+    uint16_t kcp = read16le(buf + KERNEL_CODE_PROPERTIES_OFFSET);
+    bool enableWave32 = kcp & KERNEL_CODE_PROPERTY_ENABLE_WAVEFRONT_SIZE32;
+
+    // Patch compute_pgm_rsrc1: preserve constant bits, replace VGPR/SGPR blocks
+    uint32_t rsrc1 = read32le(buf + COMPUTE_PGM_RSRC1_OFFSET);
+    uint32_t vgprBlocks = AMDGPU::IsaInfo::getEncodedNumVGPRBlocks(
+        *sti, res.totalVGPR, enableWave32);
+    uint32_t sgprBlocks =
+        sgprBlocksAlwaysZero
+            ? 0
+            : AMDGPU::IsaInfo::getNumSGPRBlocks(*sti, res.totalSGPR);
+    rsrc1 &= ~COMPUTE_PGM_RSRC1_GRANULATED_WORKITEM_VGPR_COUNT;
+    rsrc1 |=
+        (vgprBlocks << COMPUTE_PGM_RSRC1_GRANULATED_WORKITEM_VGPR_COUNT_SHIFT) &
+        COMPUTE_PGM_RSRC1_GRANULATED_WORKITEM_VGPR_COUNT;
+    rsrc1 &= ~COMPUTE_PGM_RSRC1_GRANULATED_WAVEFRONT_SGPR_COUNT;
+    rsrc1 |= (sgprBlocks
+              << COMPUTE_PGM_RSRC1_GRANULATED_WAVEFRONT_SGPR_COUNT_SHIFT) &
+             COMPUTE_PGM_RSRC1_GRANULATED_WAVEFRONT_SGPR_COUNT;
+    write32le(buf + COMPUTE_PGM_RSRC1_OFFSET, rsrc1);
+
+    // Patch compute_pgm_rsrc2: update scratch enable bit
+    uint32_t rsrc2 = read32le(buf + COMPUTE_PGM_RSRC2_OFFSET);
+    rsrc2 &= ~COMPUTE_PGM_RSRC2_ENABLE_PRIVATE_SEGMENT;
+    if (res.scratchEnable)
+      rsrc2 |= COMPUTE_PGM_RSRC2_ENABLE_PRIVATE_SEGMENT;
+    write32le(buf + COMPUTE_PGM_RSRC2_OFFSET, rsrc2);
+
+    // Patch compute_pgm_rsrc3: update AccumOffset for GFX90A
+    if (hasAccumOffset) {
+      uint32_t rsrc3 = read32le(buf + COMPUTE_PGM_RSRC3_OFFSET);
+      rsrc3 &= ~COMPUTE_PGM_RSRC3_GFX90A_ACCUM_OFFSET;
+      rsrc3 |=
+          (res.accumOffset << COMPUTE_PGM_RSRC3_GFX90A_ACCUM_OFFSET_SHIFT) &
+          COMPUTE_PGM_RSRC3_GFX90A_ACCUM_OFFSET;
+      write32le(buf + COMPUTE_PGM_RSRC3_OFFSET, rsrc3);
+    }
+
+    // Patch compute_pgm_rsrc3: update NAMED_BAR_CNT for GFX1250
+    if (hasBarriers && hasNamedBarCnt) {
+      uint32_t rsrc3 = read32le(buf + COMPUTE_PGM_RSRC3_OFFSET);
+      rsrc3 &= ~COMPUTE_PGM_RSRC3_GFX125_NAMED_BAR_CNT;
+      rsrc3 |=
+          (res.namedBarCnt << COMPUTE_PGM_RSRC3_GFX125_NAMED_BAR_CNT_SHIFT) &
+          COMPUTE_PGM_RSRC3_GFX125_NAMED_BAR_CNT;
+      write32le(buf + COMPUTE_PGM_RSRC3_OFFSET, rsrc3);
+      LLVM_DEBUG(dbgs() << "  patched " << name
+                        << ".kd NAMED_BAR_CNT=" << res.namedBarCnt << "\n");
+    }
+
+    // Patch kernel_code_properties: update USES_DYNAMIC_STACK
+    kcp &= ~KERNEL_CODE_PROPERTY_USES_DYNAMIC_STACK;
+    if (res.usesDynamicStack)
+      kcp |= KERNEL_CODE_PROPERTY_USES_DYNAMIC_STACK;
+    write16le(buf + KERNEL_CODE_PROPERTIES_OFFSET, kcp);
+
+    LLVM_DEBUG(dbgs() << "  patched " << name << ".kd: scratch="
+                      << res.scratchSize << " vgprBlocks=" << vgprBlocks
+                      << " sgprBlocks=" << sgprBlocks
+                      << " dynstack=" << res.usesDynamicStack << "\n");
+  }
+}
+
+//===----------------------------------------------------------------------===//
+// HSA metadata patching
+//===----------------------------------------------------------------------===//
+
+// Rewrite AMDHSA metadata notes so the YAML/msgpack-visible kernel resource
+// fields match the patched kernel descriptors.
+template <class ELFT>
+static void patchHSAMetadata(Ctx &ctx, AMDGPUCallGraph &cg,
+                             const MCSubtargetInfo *sti, bool hasLDS) {
+  bool hasRes = false;
+  for (CGNode *k : cg.kernels())
+    if (k->hasPropagatedRes) {
+      hasRes = true;
+      break;
+    }
+  if (!hasLDS && !hasRes)
+    return;
+
+  DenseMap<StringRef, CGNode *> nameToKernel;
+  for (CGNode *kernel : cg.kernels())
+    nameToKernel[kernel->sym->getName()] = kernel;
+
+  for (InputSectionBase *sec : ctx.inputSections) {
+    auto *isec = dyn_cast<InputSection>(sec);
+    if (!isec || isec->type != SHT_NOTE)
+      continue;
+    if (isec->name != ".note")
+      continue;
+
+    ArrayRef<uint8_t> data = isec->contentMaybeDecompress();
+    if (data.size() < 12)
+      continue;
+
+    uint32_t nameSize = read32le(data.data());
+    uint32_t descSize = read32le(data.data() + 4);
+    uint32_t noteType = read32le(data.data() + 8);
+
+    // NT_AMDGPU_METADATA = 32
+    if (noteType != 32)
+      continue;
+
+    uint32_t nameOff = 12;
+    uint32_t namePadded = alignTo(nameSize, 4);
+    uint32_t descOff = nameOff + namePadded;
+
+    if (descOff + descSize > data.size())
+      continue;
+
+    ArrayRef<uint8_t> msgpackData = data.slice(descOff, descSize);
+    msgpack::Document doc;
+    if (!doc.readFromBlob(
+            StringRef(reinterpret_cast<const char *>(msgpackData.data()),
+                      msgpackData.size()),
+            false))
+      continue;
+
+    msgpack::MapDocNode root = doc.getRoot().getMap();
+    msgpack::DocNode kernelsNode = root["amdhsa.kernels"];
+    if (kernelsNode.isEmpty())
+      continue;
+
+    bool modified = false;
+    msgpack::ArrayDocNode kernelsArray = kernelsNode.getArray();
+    for (size_t i = 0, e = kernelsArray.size(); i < e; ++i) {
+      msgpack::MapDocNode kernMap = kernelsArray[i].getMap();
+      msgpack::DocNode nameNode = kernMap[".name"];
+      if (nameNode.isEmpty())
+        continue;
+
+      StringRef kernName = nameNode.getString();
+      auto it = nameToKernel.find(kernName);
+      if (it == nameToKernel.end())
+        continue;
+      CGNode *kernel = it->second;
+
+      if (hasLDS) {
+        kernMap[".group_segment_fixed_size"] = doc.getNode(kernel->ldsSize);
+        modified = true;
+      }
+
+      if (kernel->hasPropagatedRes) {
+        ResolvedKernelResources res = resolveKernelResources(*kernel, *sti);
+        kernMap[".sgpr_count"] = doc.getNode(res.totalSGPR);
+        kernMap[".vgpr_count"] = doc.getNode(res.totalVGPR);
+        kernMap[".agpr_count"] = doc.getNode(res.numAccVGPR);
+        kernMap[".private_segment_fixed_size"] = doc.getNode(res.scratchSize);
+        kernMap[".uses_dynamic_stack"] = doc.getNode(res.usesDynamicStack);
+        modified = true;
+      }
+    }
+
+    if (!modified)
+      continue;
+
+    std::string newMsgpack;
+    doc.writeToBlob(newMsgpack);
+
+    uint32_t newDescSize = newMsgpack.size();
+    uint32_t newDescPadded = alignTo(newDescSize, 4);
+    uint32_t newSize = nameOff + namePadded + newDescPadded;
+
+    auto *buf = ctx.bAlloc.Allocate<uint8_t>(newSize);
+    memset(buf, 0, newSize);
+    write32le(buf, nameSize);
+    write32le(buf + 4, newDescSize);
+    write32le(buf + 8, noteType);
+    memcpy(buf + nameOff, data.data() + nameOff, namePadded);
+    memcpy(buf + nameOff + namePadded, newMsgpack.data(), newMsgpack.size());
+
+    isec->content_ = buf;
+    isec->size = newSize;
+  }
+}
+
+//===----------------------------------------------------------------------===//
+// Pre-link compatibility checks
+//===----------------------------------------------------------------------===//
+
+// Verify that all input files participating in object linking have compatible
+// target IDs (GPU architecture and target features). Files with different mach
+// types or incompatible xnack/sramecc settings cannot be linked.
+template <class ELFT> static bool validateTargetID(Ctx &ctx) {
+  ObjFile<ELFT> *firstObj = nullptr;
+  uint32_t firstMach = 0;
+  uint32_t firstXnack = 0;
+  uint32_t firstSramEcc = 0;
+
+  for (ELFFileBase *file : ctx.objectFiles) {
+    auto *obj = cast<ObjFile<ELFT>>(file);
+    if (!hasAMDGPUInfoSection(obj))
+      continue;
+
+    uint32_t eflags = obj->getObj().getHeader().e_flags;
+    uint32_t mach = eflags & ELF::EF_AMDGPU_MACH;
+    uint32_t xnack = eflags & ELF::EF_AMDGPU_FEATURE_XNACK_V4;
+    uint32_t sramEcc = eflags & ELF::EF_AMDGPU_FEATURE_SRAMECC_V4;
+
+    if (!firstObj) {
+      firstObj = obj;
+      firstMach = mach;
+      firstXnack = xnack;
+      firstSramEcc = sramEcc;
+      continue;
+    }
+
+    if (mach != firstMach) {
+      Err(ctx) << "AMDGPU object linking: incompatible GPU architecture "
+               << "between " << firstObj->getName() << " and "
+               << obj->getName();
+      return false;
+    }
+
+    if (xnack != firstXnack && xnack != ELF::EF_AMDGPU_FEATURE_XNACK_ANY_V4 &&
+        firstXnack != ELF::EF_AMDGPU_FEATURE_XNACK_ANY_V4) {
+      Err(ctx) << "AMDGPU object linking: incompatible xnack setting "
+               << "between " << firstObj->getName() << " and "
+               << obj->getName();
+      return false;
+    }
+
+    if (sramEcc != firstSramEcc &&
+        sramEcc != ELF::EF_AMDGPU_FEATURE_SRAMECC_ANY_V4 &&
+        firstSramEcc != ELF::EF_AMDGPU_FEATURE_SRAMECC_ANY_V4) {
+      Err(ctx) << "AMDGPU object linking: incompatible sramecc setting "
+               << "between " << firstObj->getName() << " and "
+               << obj->getName();
+      return false;
+    }
+  }
+
+  return true;
+}
+
+// Validate wave size compatibility across call edges. A caller and callee must
+// use the same wavefront size because the calling convention, register layout,
+// and instruction semantics differ between wave32 and wave64.
+static bool validateWaveSizeCompatibility(Ctx &ctx, AMDGPUCallGraph &cg) {
+  bool valid = true;
+  for (auto &[sym, node] : cg) {
+    // Alias nodes remain in the graph map after resolveAliases, but all edges
+    // to them have been redirected to the canonical node with resource info.
+    if (!node->hasLocalRes)
+      continue;
+    for (CGNode *callee : node->callees) {
+      assert(callee->hasLocalRes && "missing local resource info for callee");
+      if (node->localRes.waveSize != callee->localRes.waveSize) {
+        Err(ctx) << "AMDGPU object linking: wave size mismatch in call from '"
+                 << node->sym->getName() << "' (wave" << node->localRes.waveSize
+                 << ") to '" << callee->sym->getName() << "' (wave"
+                 << callee->localRes.waveSize << ")";
+        valid = false;
+      }
+    }
+  }
+  return valid;
+}
+
+//===----------------------------------------------------------------------===//
+// Main entry point
+//===----------------------------------------------------------------------===//
+
+// Resolve AMDGPU object-linking metadata for one ELF class. This is run after
+// regular symbol resolution so all referenced symbols and kernel descriptors
+// are available for graph construction and patching.
+template <class ELFT> void elf::resolveAMDGPUObjectLinking(Ctx &ctx) {
+  llvm::TimeTraceScope timeScope("Resolve AMDGPU Object Linking");
+
+  LLVM_DEBUG(dbgs() << "AMDGPU: validating target ID compatibility\n");
+  if (!validateTargetID<ELFT>(ctx))
+    return;
+
+  LLVM_DEBUG(dbgs() << "AMDGPU: collecting LDS symbols\n");
+  SmallVector<LDSSymbolInfo, 16> ldsSymbols;
+  SmallVector<NamedBarrierInfo, 4> barriers;
+  collectLDSSymbols(ctx, ldsSymbols, barriers);
+  LLVM_DEBUG(dbgs() << "AMDGPU: found " << ldsSymbols.size() << " LDS symbols, "
+                    << barriers.size() << " named barriers\n");
+
+  // Build sym->index maps once (used by section parsers).
+  DenseMap<Symbol *, size_t> ldsSymToIndex;
+  for (size_t i = 0, e = ldsSymbols.size(); i < e; ++i)
+    ldsSymToIndex[ldsSymbols[i].sym] = i;
+  DenseMap<Symbol *, size_t> barSymToIndex;
+  for (size_t i = 0, e = barriers.size(); i < e; ++i)
+    barSymToIndex[barriers[i].sym] = i;
+
+  AMDGPUCallGraph cg;
+
+  LLVM_DEBUG(dbgs() << "AMDGPU: parsing sections\n");
+  bool hasResourceUsage = false;
+  for (ELFFileBase *file : ctx.objectFiles) {
+    auto *obj = cast<ObjFile<ELFT>>(file);
+    parseInfoSection(obj, cg, ldsSymToIndex, barSymToIndex);
+    if (hasAMDGPUInfoSection(obj))
+      hasResourceUsage = true;
+  }
+  LLVM_DEBUG(dbgs() << "AMDGPU: building indirect call edges\n");
+  cg.buildIndirectEdges();
+
+  LLVM_DEBUG(dbgs() << "AMDGPU: resolving function aliases\n");
+  cg.resolveAliases();
+
+  LLVM_DEBUG(dbgs() << "AMDGPU: validating wave size compatibility\n");
+  if (!validateWaveSizeCompatibility(ctx, cg))
+    return;
+
+  LLVM_DEBUG(dbgs() << "AMDGPU: identifying kernels\n");
+  markKernelsWithDescriptors(ctx, cg);
+
+  // Detect resource usage from any node in the call graph.
+  if (!hasResourceUsage)
+    hasResourceUsage = cg.begin() != cg.end();
+
+  LLVM_DEBUG(dbgs() << "AMDGPU: " << ldsSymbols.size() << " regular LDS, "
+                    << barriers.size() << " named barriers\n");
+  LLVM_DEBUG(dbgs() << "AMDGPU: " << cg.kernels().size() << " kernels\n");
+  LLVM_DEBUG(if (hasResourceUsage) dbgs()
+             << "AMDGPU: has resource usage data\n");
+
+  if (ldsSymbols.empty() && barriers.empty() && !hasResourceUsage) {
+    LLVM_DEBUG(dbgs() << "AMDGPU: nothing to resolve\n");
+    return;
+  }
+
+  // Construct MCSubtargetInfo from merged ELF e_flags for target-aware
+  // resource computation (VGPR totals, extra SGPRs, etc.).
+  uint32_t eflags = ctx.arg.eflags;
+  StringRef cpu = AMDGPU::getArchNameFromElfMach(eflags & ELF::EF_AMDGPU_MACH);
+  std::string features;
+  auto addFeature = [&](StringRef feature) {
+    if (!features.empty())
+      features += ",";
+    features.append(feature.data(), feature.size());
+  };
+  if ((eflags & ELF::EF_AMDGPU_FEATURE_XNACK_V4) ==
+      ELF::EF_AMDGPU_FEATURE_XNACK_ON_V4)
+    addFeature("+xnack");
+  if ((eflags & ELF::EF_AMDGPU_FEATURE_SRAMECC_V4) ==
+      ELF::EF_AMDGPU_FEATURE_SRAMECC_ON_V4)
+    addFeature("+sramecc");
+  else if ((eflags & ELF::EF_AMDGPU_FEATURE_SRAMECC_V4) ==
+           ELF::EF_AMDGPU_FEATURE_SRAMECC_OFF_V4)
+    addFeature("-sramecc");
+
+  if (ctx.arg.osabi != ELF::ELFOSABI_AMDGPU_HSA) {
+    Err(ctx) << "AMDGPU object linking is only supported for amdhsa (OSABI "
+             << ctx.arg.osabi << ")";
+    return;
+  }
+
+  std::string error;
+  Triple triple(Triple::amdgcn, Triple::NoSubArch, Triple::AMD, Triple::AMDHSA);
+  const Target *target = TargetRegistry::lookupTarget(triple, error);
+  if (!target) {
+    Err(ctx) << "AMDGPU: failed to look up target: " << error;
+    return;
+  }
+  std::unique_ptr<MCSubtargetInfo> sti(
+      target->createMCSubtargetInfo(triple, cpu, features));
+  if (!sti) {
+    Err(ctx) << "AMDGPU: failed to create subtarget info for '" << cpu << "'";
+    return;
+  }
+
+  bool hasLDS = !ldsSymbols.empty();
+  bool hasBarriers = !barriers.empty();
+  bool needsReachability =
+      (cg.hasLDSUses() || cg.hasBarrierUses()) && !cg.kernels().empty();
+  bool needsResource = hasResourceUsage && !cg.kernels().empty();
+
+  // Alias resolution and kernel discovery must be complete before this point:
+  // the shared SCC graph is intentionally kernel-reachable and is consumed by
+  // both reachability propagation and resource propagation.
+  if (needsReachability || needsResource) {
+    LLVM_DEBUG(dbgs() << "AMDGPU: building condensed call graph\n");
+    if (!cg.buildCondensedGraph(ctx, needsResource))
+      return;
+  }
+
+  if (needsReachability) {
+    LLVM_DEBUG(dbgs() << "AMDGPU: computing kernel reachability\n");
+    cg.computeKernelReachability();
+  }
+
+  if (hasLDS)
+    resolveLDS(ctx, ldsSymbols, cg);
+
+  if (hasBarriers)
+    resolveNamedBarriers(ctx, barriers, cg);
+
+  if (needsResource) {
+    LLVM_DEBUG(dbgs() << "AMDGPU: propagating resource usage\n");
+    cg.propagateResourceUsage();
+    if (!validateKernelLDSOccupancy(ctx, cg, *sti))
+      return;
+  }
+
+  LLVM_DEBUG(dbgs() << "AMDGPU: patching kernel descriptors\n");
+  patchKernelDescriptors(ctx, cg, sti.get(), hasLDS, hasBarriers);
+
+  LLVM_DEBUG(dbgs() << "AMDGPU: patching HSA metadata\n");
+  patchHSAMetadata<ELFT>(ctx, cg, sti.get(), hasLDS);
+}
+
+template void elf::resolveAMDGPUObjectLinking<ELF32LE>(Ctx &);
+template void elf::resolveAMDGPUObjectLinking<ELF64LE>(Ctx &);
diff --git a/lld/ELF/AMDGPUObjectLinking.h b/lld/ELF/AMDGPUObjectLinking.h
new file mode 100644
index 0000000000000..ae961df6c2fd6
--- /dev/null
+++ b/lld/ELF/AMDGPUObjectLinking.h
@@ -0,0 +1,25 @@
+//===- AMDGPUObjectLinking.h - AMDGPU link-time resolution ------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Implements link-time resolution for AMDGPU object linking. This includes
+// LDS (Local Data Share) offset assignment across translation units and
+// resource usage propagation through the cross-TU call graph.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLD_ELF_AMDGPUOBJECTLINKING_H
+#define LLD_ELF_AMDGPUOBJECTLINKING_H
+
+namespace lld::elf {
+struct Ctx;
+
+template <class ELFT> void resolveAMDGPUObjectLinking(Ctx &ctx);
+
+} // namespace lld::elf
+
+#endif // LLD_ELF_AMDGPUOBJECTLINKING_H
diff --git a/lld/ELF/Arch/AMDGPU.cpp b/lld/ELF/Arch/AMDGPU.cpp
index 52fc779855a36..6a3a39727b455 100644
--- a/lld/ELF/Arch/AMDGPU.cpp
+++ b/lld/ELF/Arch/AMDGPU.cpp
@@ -151,6 +151,7 @@ uint32_t AMDGPU::calcEFlags() const {
 void AMDGPU::relocate(uint8_t *loc, const Relocation &rel, uint64_t val) const {
   switch (rel.type) {
   case R_AMDGPU_ABS32:
+  case R_AMDGPU_ABS32_LO:
   case R_AMDGPU_GOTPCREL:
   case R_AMDGPU_GOTPCREL32_LO:
   case R_AMDGPU_REL32:
@@ -180,6 +181,7 @@ RelExpr AMDGPU::getRelExpr(RelType type, const Symbol &s,
                            const uint8_t *loc) const {
   switch (type) {
   case R_AMDGPU_ABS32:
+  case R_AMDGPU_ABS32_LO:
   case R_AMDGPU_ABS64:
     return R_ABS;
   case R_AMDGPU_REL32:
diff --git a/lld/ELF/CMakeLists.txt b/lld/ELF/CMakeLists.txt
index e22897c2789d8..f258682b3ec71 100644
--- a/lld/ELF/CMakeLists.txt
+++ b/lld/ELF/CMakeLists.txt
@@ -20,6 +20,7 @@ endif()
 
 add_lld_library(lldELF
   AArch64ErrataFix.cpp
+  AMDGPUObjectLinking.cpp
   Arch/AArch64.cpp
   Arch/AMDGPU.cpp
   Arch/ARM.cpp
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index 7ec7dfcae6bca..5b74360577f40 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -23,6 +23,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "Driver.h"
+#include "AMDGPUObjectLinking.h"
 #include "Config.h"
 #include "ICF.h"
 #include "InputFiles.h"
@@ -2666,6 +2667,9 @@ static void replaceCommonSymbols(Ctx &ctx) {
       auto *s = dyn_cast<CommonSymbol>(sym);
       if (!s)
         continue;
+      // AMDGPU LDS symbols are resolved by the link-time LDS pass, not here.
+      if (sym->isAMDGPULDS)
+        continue;
 
       auto *bss = make<BssSection>(ctx, "COMMON", s->size, s->alignment);
       bss->file = s->file;
@@ -3544,6 +3548,17 @@ template <class ELFT> void LinkerDriver::link(opt::InputArgList &args) {
   if (ctx.arg.emachine == EM_RISCV)
     mergeRISCVAttributesSections(ctx);
 
+  // Resolve AMDGPU link-time LDS and resource usage before relocation
+  // resolution.
+  if (ctx.arg.emachine == EM_AMDGPU) {
+    if constexpr (ELFT::Endianness == endianness::little) {
+      resolveAMDGPUObjectLinking<ELFT>(ctx);
+    } else {
+      Err(ctx) << "AMDGPU only supports little-endian ELF";
+      return;
+    }
+  }
+
   {
     llvm::TimeTraceScope timeScope("Assign sections");
 
diff --git a/lld/ELF/InputFiles.cpp b/lld/ELF/InputFiles.cpp
index 2f544f0fe0958..c6912ecf85958 100644
--- a/lld/ELF/InputFiles.cpp
+++ b/lld/ELF/InputFiles.cpp
@@ -783,6 +783,13 @@ void ObjFile<ELFT>::initializeSections(bool ignoreComdats,
     if ((sec.sh_flags & SHF_EXCLUDE) && !ctx.arg.relocatable) {
       if (type == SHT_LLVM_CALL_GRAPH_PROFILE)
         cgProfileSectionIndex = i;
+      if (type == SHT_PROGBITS || type == SHT_STRTAB) {
+        StringRef name = CHECK2(obj.getSectionName(sec, shstrtab), this);
+        if (name == ".amdgpu.info")
+          amdgpuInfoSectionIndex = i;
+        else if (name == ".amdgpu.strtab")
+          amdgpuStrtabSectionIndex = i;
+      }
       if (type == SHT_LLVM_ADDRSIG) {
         // We ignore the address-significance table if we know that the object
         // file was created by objcopy or ld -r. This is because these tools
@@ -1225,6 +1232,20 @@ void ObjFile<ELFT>::initializeSymbols(const object::ELFFile<ELFT> &obj) {
       continue;
     }
 
+    // SHN_AMDGPU_LDS symbols encode alignment in st_value and size in st_size,
+    // like SHN_COMMON. They are resolved by the AMDGPU link-time LDS pass.
+    if (LLVM_UNLIKELY(eSym.st_shndx == ELF::SHN_AMDGPU_LDS)) {
+      if (value == 0 || value >= UINT32_MAX)
+        Err(ctx) << this << ": AMDGPU LDS symbol '" << sym->getName()
+                 << "' has invalid alignment: " << value;
+      hasCommonSyms = true;
+      CommonSymbol ldsSym{ctx,     this, StringRef(), binding,
+                          stOther, type, value,       size};
+      ldsSym.isAMDGPULDS = true;
+      sym->resolve(ctx, ldsSym);
+      continue;
+    }
+
     // Handle global defined symbols. Defined::section will be set in postParse.
     sym->resolve(ctx, Defined{ctx, this, StringRef(), binding, stOther, type,
                               value, size, nullptr});
diff --git a/lld/ELF/InputFiles.h b/lld/ELF/InputFiles.h
index aef599102ecfc..3ff4d46290fb9 100644
--- a/lld/ELF/InputFiles.h
+++ b/lld/ELF/InputFiles.h
@@ -271,6 +271,10 @@ template <class ELFT> class ObjFile : public ELFFileBase {
   // SHT_LLVM_CALL_GRAPH_PROFILE section index.
   uint32_t cgProfileSectionIndex = 0;
 
+  // .amdgpu.info / .amdgpu.strtab section indices for link-time resolution.
+  uint32_t amdgpuInfoSectionIndex = 0;
+  uint32_t amdgpuStrtabSectionIndex = 0;
+
   // MIPS GP0 value defined by this file. This value represents the gp value
   // used to create the relocatable object and required to support
   // R_MIPS_GPREL16 / R_MIPS_GPREL32 relocations.
diff --git a/lld/ELF/Symbols.cpp b/lld/ELF/Symbols.cpp
index 7bad4ceccec33..0e8c8e6304b33 100644
--- a/lld/ELF/Symbols.cpp
+++ b/lld/ELF/Symbols.cpp
@@ -599,6 +599,7 @@ void Symbol::resolve(Ctx &ctx, const CommonSymbol &other) {
   if (CommonSymbol *oldSym = dyn_cast<CommonSymbol>(this)) {
     if (ctx.arg.warnCommon)
       Warn(ctx) << "multiple common of " << getName();
+    isAMDGPULDS |= other.isAMDGPULDS;
     oldSym->alignment = std::max(oldSym->alignment, other.alignment);
     if (oldSym->size < other.size) {
       oldSym->file = other.file;
diff --git a/lld/ELF/Symbols.h b/lld/ELF/Symbols.h
index b7d0b13ae476c..6200a2f034f1d 100644
--- a/lld/ELF/Symbols.h
+++ b/lld/ELF/Symbols.h
@@ -241,7 +241,7 @@ class Symbol {
         traced(false), hasVersionSuffix(false), isInIplt(false),
         gotInIgot(false), folded(false), archSpecificBit(false),
         scriptDefined(false), dsoDefined(false), dsoProtected(false),
-        versionScriptAssigned(false), thunkAccessed(false),
+        isAMDGPULDS(false), versionScriptAssigned(false), thunkAccessed(false),
         inDynamicList(false), referenced(false), referencedAfterWrap(false) {}
 
   void overwrite(Symbol &sym, Kind k) const {
@@ -252,6 +252,7 @@ class Symbol {
     sym.binding = binding;
     sym.stOther = (stOther & ~3) | sym.visibility();
     sym.symbolKind = k;
+    sym.isAMDGPULDS = isAMDGPULDS;
   }
 
 public:
@@ -299,6 +300,11 @@ class Symbol {
   LLVM_PREFERRED_TYPE(bool)
   uint8_t dsoProtected : 1;
 
+  // True if this is an AMDGPU LDS symbol (SHN_AMDGPU_LDS). These are handled
+  // by the AMDGPU link-time LDS resolution pass, not replaceCommonSymbols().
+  LLVM_PREFERRED_TYPE(bool)
+  uint8_t isAMDGPULDS : 1;
+
   // Temporary flags used to communicate which symbol entries need PLT and GOT
   // entries during postScanRelocations();
   std::atomic<uint16_t> flags = 0;
diff --git a/lld/test/ELF/amdgpu-lds-link-time-dynlds-alias.s b/lld/test/ELF/amdgpu-lds-link-time-dynlds-alias.s
new file mode 100644
index 0000000000000..6c74784f99fa3
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-dynlds-alias.s
@@ -0,0 +1,187 @@
+# REQUIRES: amdgpu
+
+## Test that dynamic LDS variables are placed at the current frontier using
+## their own alignment. The kernel directly uses dyn_a (align=4) and calls a
+## device function in another TU that uses dyn_b (align=16).
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/a.s -o %t/a.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/b.s -o %t/b.o
+# RUN: ld.lld %t/a.o %t/b.o -o %t/out
+
+## Dynamic LDS symbols do not advance the kernel frontier.
+# RUN: llvm-readelf -s %t/out | FileCheck %s --check-prefix=SYM
+
+## group_segment_fixed_size includes the alignment padding for the dynamic base.
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+## LDS layout:
+##   small_lds: align=4, size=12 -> offset 0
+##   dyn_a (align=4): offset 12
+##   dyn_b (align=16): offset 16
+##
+## group_segment_fixed_size = 16
+
+# SYM-DAG: 0000000000000000 {{.*}} small_lds
+# SYM-DAG: 000000000000000c {{.*}} dyn_a
+# SYM-DAG: 0000000000000010 {{.*}} dyn_b
+
+# META: .group_segment_fixed_size: 16
+# META: .name: my_kernel
+
+#--- a.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	my_kernel                       ; -- Begin function my_kernel
+	.p2align	8
+	.type	my_kernel, at function
+my_kernel:
+	s_endpgm
+.Lfunc_end0:
+	.size	my_kernel, .Lfunc_end0-my_kernel
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	my_kernel.kd
+	.type	my_kernel.kd, at object
+	.size	my_kernel.kd, 64
+	.protected	my_kernel
+my_kernel.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	my_kernel at rel64-my_kernel.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Lmy_kernel.num_vgpr, 32
+	.set .Lmy_kernel.num_agpr, 0
+	.set .Lmy_kernel.numbered_sgpr, 33
+	.set .Lmy_kernel.num_named_barrier, 0
+	.set .Lmy_kernel.private_seg_size, 0
+	.set .Lmy_kernel.uses_vcc, 1
+	.set .Lmy_kernel.uses_flat_scratch, 1
+	.set .Lmy_kernel.has_dyn_sized_stack, 0
+	.set .Lmy_kernel.has_recursion, 0
+	.set .Lmy_kernel.has_indirect_call, 0
+	.amdgpu_info my_kernel
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_use small_lds
+		.amdgpu_use dyn_a
+		.amdgpu_call device_func
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	small_lds
+	.amdgpu_lds small_lds, 12, 16
+	.globl	dyn_a
+	.amdgpu_lds dyn_a, 0, 4
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           my_kernel
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         my_kernel.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- b.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	device_func                     ; -- Begin function device_func
+	.p2align	6
+	.type	device_func, at function
+device_func:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	device_func, .Lfunc_end0-device_func
+	.set .Ldevice_func.num_vgpr, 2
+	.set .Ldevice_func.num_agpr, 0
+	.set .Ldevice_func.numbered_sgpr, 32
+	.set .Ldevice_func.num_named_barrier, 0
+	.set .Ldevice_func.private_seg_size, 0
+	.set .Ldevice_func.uses_vcc, 0
+	.set .Ldevice_func.uses_flat_scratch, 0
+	.set .Ldevice_func.has_dyn_sized_stack, 0
+	.set .Ldevice_func.has_recursion, 0
+	.set .Ldevice_func.has_indirect_call, 0
+	.amdgpu_info device_func
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_use dyn_b
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 2
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 32
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	dyn_b
+	.amdgpu_lds dyn_b, 0, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:  []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-dynlds-align.s b/lld/test/ELF/amdgpu-lds-link-time-dynlds-align.s
new file mode 100644
index 0000000000000..b1c3bd5db8a32
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-dynlds-align.s
@@ -0,0 +1,182 @@
+# REQUIRES: amdgpu
+
+## Test that size-0 LDS symbols (dynamic LDS) with high alignment are
+## correctly padded past the static LDS region.
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/a.s -o %t/a.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/b.s -o %t/b.o
+# RUN: ld.lld %t/a.o %t/b.o -o %t/out
+
+## Verify symbol offsets.
+# RUN: llvm-readelf -s %t/out | FileCheck %s --check-prefix=SYM
+
+## Verify group_segment_fixed_size in HSA metadata.
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+## LDS layout:
+##   small_lds: align=4, size=12 -> offset 0
+##   dyn_lds: align=128, size=0 -> offset 128
+##     (12 bytes of static, then padded to 128 for alignment)
+##
+## group_segment_fixed_size = 128 (the dynlds offset with alignment padding).
+
+# SYM-DAG: 0000000000000000 {{.*}} small_lds
+# SYM-DAG: 0000000000000080 {{.*}} dyn_lds
+
+# META: .group_segment_fixed_size: 128
+# META: .name: my_kernel
+
+#--- a.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	my_kernel                       ; -- Begin function my_kernel
+	.p2align	8
+	.type	my_kernel, at function
+my_kernel:
+	s_endpgm
+.Lfunc_end0:
+	.size	my_kernel, .Lfunc_end0-my_kernel
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	my_kernel.kd
+	.type	my_kernel.kd, at object
+	.size	my_kernel.kd, 64
+	.protected	my_kernel
+my_kernel.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	my_kernel at rel64-my_kernel.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Lmy_kernel.num_vgpr, 32
+	.set .Lmy_kernel.num_agpr, 0
+	.set .Lmy_kernel.numbered_sgpr, 33
+	.set .Lmy_kernel.num_named_barrier, 0
+	.set .Lmy_kernel.private_seg_size, 0
+	.set .Lmy_kernel.uses_vcc, 1
+	.set .Lmy_kernel.uses_flat_scratch, 1
+	.set .Lmy_kernel.has_dyn_sized_stack, 0
+	.set .Lmy_kernel.has_recursion, 0
+	.set .Lmy_kernel.has_indirect_call, 0
+	.amdgpu_info my_kernel
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_use small_lds
+		.amdgpu_use dyn_lds
+		.amdgpu_call helper
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	small_lds
+	.amdgpu_lds small_lds, 12, 16
+	.globl	dyn_lds
+	.amdgpu_lds dyn_lds, 0, 128
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           my_kernel
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         my_kernel.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- b.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	helper                          ; -- Begin function helper
+	.p2align	6
+	.type	helper, at function
+helper:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	helper, .Lfunc_end0-helper
+	.set .Lhelper.num_vgpr, 0
+	.set .Lhelper.num_agpr, 0
+	.set .Lhelper.numbered_sgpr, 32
+	.set .Lhelper.num_named_barrier, 0
+	.set .Lhelper.private_seg_size, 0
+	.set .Lhelper.uses_vcc, 0
+	.set .Lhelper.uses_flat_scratch, 0
+	.set .Lhelper.has_dyn_sized_stack, 0
+	.set .Lhelper.has_recursion, 0
+	.set .Lhelper.has_indirect_call, 0
+	.amdgpu_info helper
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 0
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 32
+	.set amdgpu.max_num_named_barrier, 0
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:  []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-dynlds.s b/lld/test/ELF/amdgpu-lds-link-time-dynlds.s
new file mode 100644
index 0000000000000..f9fdd3c4d4663
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-dynlds.s
@@ -0,0 +1,184 @@
+# REQUIRES: amdgpu
+
+## Test that size-0 LDS symbols (dynamic LDS) are placed after all static LDS
+## symbols and that the kernel descriptor's group_segment_fixed_size reflects
+## the dynamic base offset.
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/a.s -o %t/a.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/b.s -o %t/b.o
+# RUN: ld.lld %t/a.o %t/b.o -o %t/out
+
+## Verify symbol offsets.
+# RUN: llvm-readelf -s %t/out | FileCheck %s --check-prefix=SYM
+
+## Verify group_segment_fixed_size in HSA metadata is patched.
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+## LDS layout:
+##   static_lds: align=16, size=256 -> offset 0
+##   dyn_lds: align=4, size=0 -> offset 256
+## Total fixed LDS = 256 = 0x100 (dynamic base)
+##
+## group_segment_fixed_size = 256 because the size-0 dyn_lds symbol
+## sits at offset 256 but contributes 0 bytes.
+
+# SYM-DAG: 0000000000000000 {{.*}} static_lds
+# SYM-DAG: 0000000000000100 {{.*}} dyn_lds
+
+# META: .group_segment_fixed_size: 256
+# META: .name: my_kernel
+
+#--- a.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	my_kernel                       ; -- Begin function my_kernel
+	.p2align	8
+	.type	my_kernel, at function
+my_kernel:
+	s_endpgm
+.Lfunc_end0:
+	.size	my_kernel, .Lfunc_end0-my_kernel
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	my_kernel.kd
+	.type	my_kernel.kd, at object
+	.size	my_kernel.kd, 64
+	.protected	my_kernel
+my_kernel.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	my_kernel at rel64-my_kernel.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Lmy_kernel.num_vgpr, 32
+	.set .Lmy_kernel.num_agpr, 0
+	.set .Lmy_kernel.numbered_sgpr, 33
+	.set .Lmy_kernel.num_named_barrier, 0
+	.set .Lmy_kernel.private_seg_size, 0
+	.set .Lmy_kernel.uses_vcc, 1
+	.set .Lmy_kernel.uses_flat_scratch, 1
+	.set .Lmy_kernel.has_dyn_sized_stack, 0
+	.set .Lmy_kernel.has_recursion, 0
+	.set .Lmy_kernel.has_indirect_call, 0
+	.amdgpu_info my_kernel
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_use static_lds
+		.amdgpu_use dyn_lds
+		.amdgpu_call helper
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	static_lds
+	.amdgpu_lds static_lds, 256, 16
+	.globl	dyn_lds
+	.amdgpu_lds dyn_lds, 0, 4
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           my_kernel
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         my_kernel.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- b.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	helper                          ; -- Begin function helper
+	.p2align	6
+	.type	helper, at function
+helper:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	helper, .Lfunc_end0-helper
+	.set .Lhelper.num_vgpr, 0
+	.set .Lhelper.num_agpr, 0
+	.set .Lhelper.numbered_sgpr, 32
+	.set .Lhelper.num_named_barrier, 0
+	.set .Lhelper.private_seg_size, 0
+	.set .Lhelper.uses_vcc, 0
+	.set .Lhelper.uses_flat_scratch, 0
+	.set .Lhelper.has_dyn_sized_stack, 0
+	.set .Lhelper.has_recursion, 0
+	.set .Lhelper.has_indirect_call, 0
+	.amdgpu_info helper
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 0
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 32
+	.set amdgpu.max_num_named_barrier, 0
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:  []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-grouped.s b/lld/test/ELF/amdgpu-lds-link-time-grouped.s
new file mode 100644
index 0000000000000..4e15f72e9df5c
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-grouped.s
@@ -0,0 +1,425 @@
+# REQUIRES: amdgpu
+
+## Test grouped LDS allocation with independent kernel groups and tier ordering.
+##
+## TU1 has:
+##   - func: uses @lds_callee (callee-scope, internal, 64 bytes)
+##   - K1: uses @lds_global (global-scope, external, 32 bytes),
+##         uses @lds_k1 (kernel-scope, internal, 16 bytes), calls func
+##   - K2: uses @lds_global, calls func (no kernel-scope LDS)
+##
+## TU2 has:
+##   - K3: uses @lds_k3 (kernel-scope, internal, 128 bytes)
+##
+## Grouping:
+##   K1, K2 share lds_callee and lds_global -> Group 1
+##   K3 is independent -> Group 2
+##
+## Group 1 tier classification:
+##   Shared tier: __amdgpu_lds.func (callee-scope, user=func not a kernel)
+##                lds_global (global-scope, users=K1,K2)
+##   Kernel tier: __amdgpu_lds.K1 (kernel-scope, user=K1 a kernel)
+##
+## Group 1 layout (per-kernel frontier order; both shared vars have use_count=2,
+## tie-broken by size -- larger first):
+##   Shared: __amdgpu_lds.func (align 16, size 64) @ 0x00
+##           lds_global (align 16, size 32) @ 0x40
+##   Kernel: __amdgpu_lds.K1 (align 16, size 16) @ 0x60
+##
+## Group 2 layout (starts from offset 0):
+##   __amdgpu_lds.K3 (align 16, size 128) @ 0x00
+##
+## Per-kernel sizes:
+##   K1: reaches func's struct(0..64), lds_global(64..96), K1's struct(96..112) -> 0x70
+##   K2: reaches func's struct(0..64), lds_global(64..96) -> 0x60
+##   K3: reaches K3's struct(0..128) -> 0x80
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu1.s -o %t/tu1.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu2.s -o %t/tu2.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu3.s -o %t/tu3.o
+# RUN: ld.lld %t/tu1.o %t/tu2.o %t/tu3.o -o %t/out
+# RUN: llvm-readelf -s %t/out | FileCheck %s --check-prefix=SYM
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+## LDS symbol offsets (Group 1 shared tier first, kernel tier last)
+# SYM-DAG: 0000000000000000 {{.*}} __amdgpu_lds.func
+# SYM-DAG: 0000000000000040 {{.*}} lds_global
+# SYM-DAG: 0000000000000060 {{.*}} __amdgpu_lds.K1
+
+## Group 2 allocates from offset 0 independently
+# SYM-DAG: 0000000000000000 {{.*}} __amdgpu_lds.K3
+
+## Per-kernel LDS sizes patched into the kernel descriptor and HSA metadata
+# META-DAG: .group_segment_fixed_size: 112
+# META-DAG: .group_segment_fixed_size: 96
+# META-DAG: .group_segment_fixed_size: 128
+
+#--- tu1.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	func                            ; -- Begin function func
+	.p2align	6
+	.type	func, at function
+func:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	func, .Lfunc_end0-func
+	.set .Lfunc.num_vgpr, 41
+	.set .Lfunc.num_agpr, 0
+	.set .Lfunc.numbered_sgpr, 34
+	.set .Lfunc.num_named_barrier, 0
+	.set .Lfunc.private_seg_size, 16
+	.set .Lfunc.uses_vcc, 1
+	.set .Lfunc.uses_flat_scratch, 0
+	.set .Lfunc.has_dyn_sized_stack, 0
+	.set .Lfunc.has_recursion, 0
+	.set .Lfunc.has_indirect_call, 0
+	.text
+	.globl	K1                              ; -- Begin function K1
+	.p2align	8
+	.type	K1, at function
+K1:
+	s_endpgm
+.Lfunc_end1:
+	.size	K1, .Lfunc_end1-K1
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K1.kd
+	.type	K1.kd, at object
+	.size	K1.kd, 64
+	.protected	K1
+K1.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K1 at rel64-K1.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK1.num_vgpr, 32
+	.set .LK1.num_agpr, 0
+	.set .LK1.numbered_sgpr, 33
+	.set .LK1.num_named_barrier, 0
+	.set .LK1.private_seg_size, 0
+	.set .LK1.uses_vcc, 1
+	.set .LK1.uses_flat_scratch, 1
+	.set .LK1.has_dyn_sized_stack, 0
+	.set .LK1.has_recursion, 1
+	.set .LK1.has_indirect_call, 0
+	.text
+	.globl	K2                              ; -- Begin function K2
+	.p2align	8
+	.type	K2, at function
+K2:
+	s_endpgm
+.Lfunc_end2:
+	.size	K2, .Lfunc_end2-K2
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K2.kd
+	.type	K2.kd, at object
+	.size	K2.kd, 64
+	.protected	K2
+K2.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K2 at rel64-K2.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK2.num_vgpr, 32
+	.set .LK2.num_agpr, 0
+	.set .LK2.numbered_sgpr, 33
+	.set .LK2.num_named_barrier, 0
+	.set .LK2.private_seg_size, 0
+	.set .LK2.uses_vcc, 1
+	.set .LK2.uses_flat_scratch, 1
+	.set .LK2.has_dyn_sized_stack, 0
+	.set .LK2.has_recursion, 1
+	.set .LK2.has_indirect_call, 0
+	.amdgpu_info func
+		.amdgpu_flags 1
+		.amdgpu_num_vgpr 41
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 34
+		.amdgpu_private_segment_size 16
+		.amdgpu_use __amdgpu_lds.func
+		.amdgpu_call extern_func
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K1
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_use __amdgpu_lds.K1
+		.amdgpu_use lds_global
+		.amdgpu_call func
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K2
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_use lds_global
+		.amdgpu_call func
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 41
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 34
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	lds_global
+	.amdgpu_lds lds_global, 32, 16
+	.globl	__amdgpu_lds.func
+	.amdgpu_lds __amdgpu_lds.func, 64, 16
+	.globl	__amdgpu_lds.K1
+	.amdgpu_lds __amdgpu_lds.K1, 16, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K1
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K1.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K2
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K2.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- tu2.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	K3                              ; -- Begin function K3
+	.p2align	8
+	.type	K3, at function
+K3:
+	s_endpgm
+.Lfunc_end0:
+	.size	K3, .Lfunc_end0-K3
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K3.kd
+	.type	K3.kd, at object
+	.size	K3.kd, 64
+	.protected	K3
+K3.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K3 at rel64-K3.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK3.num_vgpr, 2
+	.set .LK3.num_agpr, 0
+	.set .LK3.numbered_sgpr, 10
+	.set .LK3.num_named_barrier, 0
+	.set .LK3.private_seg_size, 0
+	.set .LK3.uses_vcc, 0
+	.set .LK3.uses_flat_scratch, 0
+	.set .LK3.has_dyn_sized_stack, 0
+	.set .LK3.has_recursion, 0
+	.set .LK3.has_indirect_call, 0
+	.amdgpu_info K3
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use __amdgpu_lds.K3
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	__amdgpu_lds.K3
+	.amdgpu_lds __amdgpu_lds.K3, 128, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K3
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K3.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- tu3.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	extern_func                     ; -- Begin function extern_func
+	.p2align	6
+	.type	extern_func, at function
+extern_func:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	extern_func, .Lfunc_end0-extern_func
+	.set .Lextern_func.num_vgpr, 0
+	.set .Lextern_func.num_agpr, 0
+	.set .Lextern_func.numbered_sgpr, 32
+	.set .Lextern_func.num_named_barrier, 0
+	.set .Lextern_func.private_seg_size, 0
+	.set .Lextern_func.uses_vcc, 0
+	.set .Lextern_func.uses_flat_scratch, 0
+	.set .Lextern_func.has_dyn_sized_stack, 0
+	.set .Lextern_func.has_recursion, 0
+	.set .Lextern_func.has_indirect_call, 0
+	.amdgpu_info extern_func
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 0
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 32
+	.set amdgpu.max_num_named_barrier, 0
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:  []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-indirect-cross-tu.s b/lld/test/ELF/amdgpu-lds-link-time-indirect-cross-tu.s
new file mode 100644
index 0000000000000..8acb6f07a76fd
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-indirect-cross-tu.s
@@ -0,0 +1,203 @@
+# REQUIRES: amdgpu
+
+## Test cross-TU address-taken: the function is defined in TU1 but its address
+## is taken in TU2. The linker should match the indirect call in TU2 with the
+## function from TU1 via prototype matching.
+##
+## TU1: defines target_func (uses lds_var)
+## TU2: defines kern which passes target_func as a pointer to caller
+##      defines caller which makes an indirect call
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu1.s -o %t/tu1.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu2.s -o %t/tu2.o
+# RUN: ld.lld %t/tu1.o %t/tu2.o -o %t/out
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+## Kernel LDS size should include lds_var (128 bytes).
+# META: .group_segment_fixed_size: 128
+
+#--- tu1.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	target_func                     ; -- Begin function target_func
+	.p2align	6
+	.type	target_func, at function
+target_func:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	target_func, .Lfunc_end0-target_func
+	.set .Ltarget_func.num_vgpr, 2
+	.set .Ltarget_func.num_agpr, 0
+	.set .Ltarget_func.numbered_sgpr, 32
+	.set .Ltarget_func.num_named_barrier, 0
+	.set .Ltarget_func.private_seg_size, 0
+	.set .Ltarget_func.uses_vcc, 0
+	.set .Ltarget_func.uses_flat_scratch, 0
+	.set .Ltarget_func.has_dyn_sized_stack, 0
+	.set .Ltarget_func.has_recursion, 0
+	.set .Ltarget_func.has_indirect_call, 0
+	.amdgpu_info target_func
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_use lds_var
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 2
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 32
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	lds_var
+	.amdgpu_lds lds_var, 128, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:  []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- tu2.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	caller                          ; -- Begin function caller
+	.p2align	6
+	.type	caller, at function
+caller:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	caller, .Lfunc_end0-caller
+	.set .Lcaller.num_vgpr, 41
+	.set .Lcaller.num_agpr, 0
+	.set .Lcaller.numbered_sgpr, 66
+	.set .Lcaller.num_named_barrier, 0
+	.set .Lcaller.private_seg_size, 16
+	.set .Lcaller.uses_vcc, 1
+	.set .Lcaller.uses_flat_scratch, 1
+	.set .Lcaller.has_dyn_sized_stack, 1
+	.set .Lcaller.has_recursion, 1
+	.set .Lcaller.has_indirect_call, 1
+	.text
+	.globl	kern                            ; -- Begin function kern
+	.p2align	8
+	.type	kern, at function
+kern:
+	s_endpgm
+.Lfunc_end1:
+	.size	kern, .Lfunc_end1-kern
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kern.kd
+	.type	kern.kd, at object
+	.size	kern.kd, 64
+	.protected	kern
+kern.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	kern at rel64-kern.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Lkern.num_vgpr, 32
+	.set .Lkern.num_agpr, 0
+	.set .Lkern.numbered_sgpr, 33
+	.set .Lkern.num_named_barrier, 0
+	.set .Lkern.private_seg_size, 0
+	.set .Lkern.uses_vcc, 1
+	.set .Lkern.uses_flat_scratch, 1
+	.set .Lkern.has_dyn_sized_stack, 0
+	.set .Lkern.has_recursion, 1
+	.set .Lkern.has_indirect_call, 0
+	.amdgpu_info caller
+		.amdgpu_flags 7
+		.amdgpu_num_vgpr 41
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 66
+		.amdgpu_private_segment_size 16
+		.amdgpu_indirect_call "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info kern
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_call caller
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info target_func
+		.amdgpu_typeid "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 41
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 66
+	.set amdgpu.max_num_named_barrier, 0
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           kern
+    .private_segment_fixed_size: 0
+    .sgpr_count:     39
+    .sgpr_spill_count: 0
+    .symbol:         kern.kd
+    .uses_dynamic_stack: true
+    .vgpr_count:     32
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-indirect-mixed.s b/lld/test/ELF/amdgpu-lds-link-time-indirect-mixed.s
new file mode 100644
index 0000000000000..cb010601677ff
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-indirect-mixed.s
@@ -0,0 +1,180 @@
+# REQUIRES: amdgpu
+
+## Test a function called both directly and indirectly. The LDS should be
+## reachable through both paths. The kernel's LDS size should include the
+## function's LDS regardless of which path discovers it.
+##
+## Call graph:
+##   kern -> target_func (direct call)
+##   kern -> caller --(indirect)--> target_func
+##   target_func -> lds_var
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu1.s -o %t/tu1.o
+# RUN: ld.lld %t/tu1.o -o %t/out
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+## Kernel LDS size should include lds_var (128 bytes).
+# META: .group_segment_fixed_size: 128
+
+#--- tu1.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	target_func                     ; -- Begin function target_func
+	.p2align	6
+	.type	target_func, at function
+target_func:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	target_func, .Lfunc_end0-target_func
+	.set .Ltarget_func.num_vgpr, 2
+	.set .Ltarget_func.num_agpr, 0
+	.set .Ltarget_func.numbered_sgpr, 32
+	.set .Ltarget_func.num_named_barrier, 0
+	.set .Ltarget_func.private_seg_size, 0
+	.set .Ltarget_func.uses_vcc, 0
+	.set .Ltarget_func.uses_flat_scratch, 0
+	.set .Ltarget_func.has_dyn_sized_stack, 0
+	.set .Ltarget_func.has_recursion, 0
+	.set .Ltarget_func.has_indirect_call, 0
+	.text
+	.globl	caller                          ; -- Begin function caller
+	.p2align	6
+	.type	caller, at function
+caller:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end1:
+	.size	caller, .Lfunc_end1-caller
+	.set .Lcaller.num_vgpr, 41
+	.set .Lcaller.num_agpr, 0
+	.set .Lcaller.numbered_sgpr, 66
+	.set .Lcaller.num_named_barrier, 0
+	.set .Lcaller.private_seg_size, 16
+	.set .Lcaller.uses_vcc, 1
+	.set .Lcaller.uses_flat_scratch, 1
+	.set .Lcaller.has_dyn_sized_stack, 1
+	.set .Lcaller.has_recursion, 1
+	.set .Lcaller.has_indirect_call, 1
+	.text
+	.globl	kern                            ; -- Begin function kern
+	.p2align	8
+	.type	kern, at function
+kern:
+	s_endpgm
+.Lfunc_end2:
+	.size	kern, .Lfunc_end2-kern
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kern.kd
+	.type	kern.kd, at object
+	.size	kern.kd, 64
+	.protected	kern
+kern.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	kern at rel64-kern.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Lkern.num_vgpr, 32
+	.set .Lkern.num_agpr, 0
+	.set .Lkern.numbered_sgpr, 33
+	.set .Lkern.num_named_barrier, 0
+	.set .Lkern.private_seg_size, 0
+	.set .Lkern.uses_vcc, 1
+	.set .Lkern.uses_flat_scratch, 1
+	.set .Lkern.has_dyn_sized_stack, 0
+	.set .Lkern.has_recursion, 1
+	.set .Lkern.has_indirect_call, 0
+	.amdgpu_info target_func
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_use lds_var
+		.amdgpu_typeid "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info caller
+		.amdgpu_flags 7
+		.amdgpu_num_vgpr 41
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 66
+		.amdgpu_private_segment_size 16
+		.amdgpu_indirect_call "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info kern
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_call target_func
+		.amdgpu_call caller
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 41
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 66
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	lds_var
+	.amdgpu_lds lds_var, 128, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           kern
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         kern.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-indirect-multi.s b/lld/test/ELF/amdgpu-lds-link-time-indirect-multi.s
new file mode 100644
index 0000000000000..7e68327083b0c
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-indirect-multi.s
@@ -0,0 +1,220 @@
+# REQUIRES: amdgpu
+
+## Test indirect call with multiple potential callees. Two address-taken
+## functions have the same prototype. Both should be considered potential
+## callees of the indirect call site, and LDS from both should be reachable.
+##
+## Call graph:
+##   my_kernel -> caller --(indirect)--> {target_a, target_b}
+##   target_a -> lds_a (64 bytes)
+##   target_b -> lds_b (32 bytes)
+##
+## Both targets: void(i32) -> encoding "vi"
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu1.s -o %t/tu1.o
+# RUN: ld.lld %t/tu1.o -o %t/out
+# RUN: llvm-readelf -s %t/out | FileCheck %s --check-prefix=SYM
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+## Both LDS variables should be resolved.
+# SYM-DAG: {{[0-9a-f]+}} {{.*}} lds_a
+# SYM-DAG: {{[0-9a-f]+}} {{.*}} lds_b
+
+## Kernel's LDS size should include both lds_a (256 bytes) and lds_b (128 bytes).
+## lds_a: align=4, size=256 -> offset 0, end 256
+## lds_b: align=4, size=128 -> offset 256, end 384
+# META: .group_segment_fixed_size: 384
+
+#--- tu1.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	target_a                        ; -- Begin function target_a
+	.p2align	6
+	.type	target_a, at function
+target_a:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	target_a, .Lfunc_end0-target_a
+	.set .Ltarget_a.num_vgpr, 2
+	.set .Ltarget_a.num_agpr, 0
+	.set .Ltarget_a.numbered_sgpr, 32
+	.set .Ltarget_a.num_named_barrier, 0
+	.set .Ltarget_a.private_seg_size, 0
+	.set .Ltarget_a.uses_vcc, 0
+	.set .Ltarget_a.uses_flat_scratch, 0
+	.set .Ltarget_a.has_dyn_sized_stack, 0
+	.set .Ltarget_a.has_recursion, 0
+	.set .Ltarget_a.has_indirect_call, 0
+	.text
+	.globl	target_b                        ; -- Begin function target_b
+	.p2align	6
+	.type	target_b, at function
+target_b:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end1:
+	.size	target_b, .Lfunc_end1-target_b
+	.set .Ltarget_b.num_vgpr, 2
+	.set .Ltarget_b.num_agpr, 0
+	.set .Ltarget_b.numbered_sgpr, 32
+	.set .Ltarget_b.num_named_barrier, 0
+	.set .Ltarget_b.private_seg_size, 0
+	.set .Ltarget_b.uses_vcc, 0
+	.set .Ltarget_b.uses_flat_scratch, 0
+	.set .Ltarget_b.has_dyn_sized_stack, 0
+	.set .Ltarget_b.has_recursion, 0
+	.set .Ltarget_b.has_indirect_call, 0
+	.text
+	.globl	caller                          ; -- Begin function caller
+	.p2align	6
+	.type	caller, at function
+caller:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end2:
+	.size	caller, .Lfunc_end2-caller
+	.set .Lcaller.num_vgpr, 41
+	.set .Lcaller.num_agpr, 0
+	.set .Lcaller.numbered_sgpr, 66
+	.set .Lcaller.num_named_barrier, 0
+	.set .Lcaller.private_seg_size, 16
+	.set .Lcaller.uses_vcc, 1
+	.set .Lcaller.uses_flat_scratch, 1
+	.set .Lcaller.has_dyn_sized_stack, 1
+	.set .Lcaller.has_recursion, 1
+	.set .Lcaller.has_indirect_call, 1
+	.text
+	.globl	my_kernel                       ; -- Begin function my_kernel
+	.p2align	8
+	.type	my_kernel, at function
+my_kernel:
+	s_endpgm
+.Lfunc_end3:
+	.size	my_kernel, .Lfunc_end3-my_kernel
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	my_kernel.kd
+	.type	my_kernel.kd, at object
+	.size	my_kernel.kd, 64
+	.protected	my_kernel
+my_kernel.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	my_kernel at rel64-my_kernel.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469514
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Lmy_kernel.num_vgpr, 42
+	.set .Lmy_kernel.num_agpr, 0
+	.set .Lmy_kernel.numbered_sgpr, 88
+	.set .Lmy_kernel.num_named_barrier, 0
+	.set .Lmy_kernel.private_seg_size, 0
+	.set .Lmy_kernel.uses_vcc, 1
+	.set .Lmy_kernel.uses_flat_scratch, 1
+	.set .Lmy_kernel.has_dyn_sized_stack, 0
+	.set .Lmy_kernel.has_recursion, 1
+	.set .Lmy_kernel.has_indirect_call, 0
+	.amdgpu_info target_a
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_use lds_a
+		.amdgpu_typeid "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info target_b
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_use lds_b
+		.amdgpu_typeid "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info caller
+		.amdgpu_flags 7
+		.amdgpu_num_vgpr 41
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 66
+		.amdgpu_private_segment_size 16
+		.amdgpu_indirect_call "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info my_kernel
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 42
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 88
+		.amdgpu_private_segment_size 0
+		.amdgpu_call caller
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 41
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 66
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	lds_a
+	.amdgpu_lds lds_a, 256, 16
+	.globl	lds_b
+	.amdgpu_lds lds_b, 128, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           my_kernel
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         my_kernel.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-indirect-nested.s b/lld/test/ELF/amdgpu-lds-link-time-indirect-nested.s
new file mode 100644
index 0000000000000..d555b4e6d9290
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-indirect-nested.s
@@ -0,0 +1,252 @@
+# REQUIRES: amdgpu
+
+## Test nested indirect calls (C++ virtual method pattern): an address-taken
+## function itself makes an indirect call to another function. Both functions
+## use LDS. The linker must discover all LDS through the indirect call chain
+## and assign sequential non-overlapping offsets.
+##
+## Call graph:
+##   kern -> caller --(indirect)--> outer_target --(indirect)--> inner_target
+##   outer_target -> lds_outer (128 bytes)
+##   inner_target -> lds_inner (64 bytes)
+##
+## Both indirect calls: void(i32) -> encoding "vi"
+## Both targets are address-taken with encoding "vi"
+##
+## All LDS must be assigned unique offsets (no slot reuse).
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu1.s -o %t/tu1.o
+# RUN: ld.lld %t/tu1.o -o %t/out
+# RUN: llvm-readelf -s %t/out | FileCheck %s --check-prefix=SYM
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+## Both LDS variables resolved with non-overlapping offsets.
+# SYM-DAG: {{[0-9a-f]+}} {{.*}} lds_outer
+# SYM-DAG: {{[0-9a-f]+}} {{.*}} lds_inner
+
+## Kernel LDS size includes both: lds_outer (128) + lds_inner (64) = 192.
+# META: .group_segment_fixed_size: 192
+
+#--- tu1.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	inner_target                    ; -- Begin function inner_target
+	.p2align	6
+	.type	inner_target, at function
+inner_target:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	inner_target, .Lfunc_end0-inner_target
+	.set .Linner_target.num_vgpr, 2
+	.set .Linner_target.num_agpr, 0
+	.set .Linner_target.numbered_sgpr, 32
+	.set .Linner_target.num_named_barrier, 0
+	.set .Linner_target.private_seg_size, 0
+	.set .Linner_target.uses_vcc, 0
+	.set .Linner_target.uses_flat_scratch, 0
+	.set .Linner_target.has_dyn_sized_stack, 0
+	.set .Linner_target.has_recursion, 0
+	.set .Linner_target.has_indirect_call, 0
+	.text
+	.globl	dispatch                        ; -- Begin function dispatch
+	.p2align	6
+	.type	dispatch, at function
+dispatch:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end1:
+	.size	dispatch, .Lfunc_end1-dispatch
+	.set .Ldispatch.num_vgpr, 41
+	.set .Ldispatch.num_agpr, 0
+	.set .Ldispatch.numbered_sgpr, 66
+	.set .Ldispatch.num_named_barrier, 0
+	.set .Ldispatch.private_seg_size, 16
+	.set .Ldispatch.uses_vcc, 1
+	.set .Ldispatch.uses_flat_scratch, 1
+	.set .Ldispatch.has_dyn_sized_stack, 1
+	.set .Ldispatch.has_recursion, 1
+	.set .Ldispatch.has_indirect_call, 1
+	.text
+	.globl	outer_target                    ; -- Begin function outer_target
+	.p2align	6
+	.type	outer_target, at function
+outer_target:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end2:
+	.size	outer_target, .Lfunc_end2-outer_target
+	.set .Louter_target.num_vgpr, 42
+	.set .Louter_target.num_agpr, 0
+	.set .Louter_target.numbered_sgpr, 66
+	.set .Louter_target.num_named_barrier, 0
+	.set .Louter_target.private_seg_size, 16
+	.set .Louter_target.uses_vcc, 1
+	.set .Louter_target.uses_flat_scratch, 0
+	.set .Louter_target.has_dyn_sized_stack, 0
+	.set .Louter_target.has_recursion, 1
+	.set .Louter_target.has_indirect_call, 0
+	.text
+	.globl	caller                          ; -- Begin function caller
+	.p2align	6
+	.type	caller, at function
+caller:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end3:
+	.size	caller, .Lfunc_end3-caller
+	.set .Lcaller.num_vgpr, 41
+	.set .Lcaller.num_agpr, 0
+	.set .Lcaller.numbered_sgpr, 66
+	.set .Lcaller.num_named_barrier, 0
+	.set .Lcaller.private_seg_size, 16
+	.set .Lcaller.uses_vcc, 1
+	.set .Lcaller.uses_flat_scratch, 1
+	.set .Lcaller.has_dyn_sized_stack, 1
+	.set .Lcaller.has_recursion, 1
+	.set .Lcaller.has_indirect_call, 1
+	.text
+	.globl	kern                            ; -- Begin function kern
+	.p2align	8
+	.type	kern, at function
+kern:
+	s_endpgm
+.Lfunc_end4:
+	.size	kern, .Lfunc_end4-kern
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kern.kd
+	.type	kern.kd, at object
+	.size	kern.kd, 64
+	.protected	kern
+kern.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	kern at rel64-kern.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Lkern.num_vgpr, 32
+	.set .Lkern.num_agpr, 0
+	.set .Lkern.numbered_sgpr, 33
+	.set .Lkern.num_named_barrier, 0
+	.set .Lkern.private_seg_size, 0
+	.set .Lkern.uses_vcc, 1
+	.set .Lkern.uses_flat_scratch, 1
+	.set .Lkern.has_dyn_sized_stack, 0
+	.set .Lkern.has_recursion, 1
+	.set .Lkern.has_indirect_call, 0
+	.amdgpu_info inner_target
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_use lds_inner
+		.amdgpu_typeid "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info dispatch
+		.amdgpu_flags 7
+		.amdgpu_num_vgpr 41
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 66
+		.amdgpu_private_segment_size 16
+		.amdgpu_indirect_call "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info outer_target
+		.amdgpu_flags 1
+		.amdgpu_num_vgpr 42
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 66
+		.amdgpu_private_segment_size 16
+		.amdgpu_use lds_outer
+		.amdgpu_call dispatch
+		.amdgpu_typeid "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info caller
+		.amdgpu_flags 7
+		.amdgpu_num_vgpr 41
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 66
+		.amdgpu_private_segment_size 16
+		.amdgpu_indirect_call "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info kern
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_call caller
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 42
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 66
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	lds_outer
+	.amdgpu_lds lds_outer, 128, 16
+	.globl	lds_inner
+	.amdgpu_lds lds_inner, 64, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           kern
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         kern.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-indirect-prototype.s b/lld/test/ELF/amdgpu-lds-link-time-indirect-prototype.s
new file mode 100644
index 0000000000000..23087a387dd97
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-indirect-prototype.s
@@ -0,0 +1,245 @@
+# REQUIRES: amdgpu
+
+## Test LDS discovery through an indirect call, with prototype-based filtering.
+## Only address-taken functions matching the indirect call's prototype should be
+## considered as potential callees.
+##
+## target_match: void(i32) -> encoding "vi" -- matches the indirect call
+## target_nomatch: i32(i32, i32) -> encoding "iii" -- does NOT match
+##
+## Only lds_match should be reachable from the kernel through the indirect edge.
+## lds_nomatch should NOT be reachable (its function has a different prototype).
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu1.s -o %t/tu1.o
+# RUN: ld.lld %t/tu1.o -o %t/out
+# RUN: llvm-readelf -s %t/out | FileCheck %s --check-prefix=SYM
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+## The matching target's LDS is discovered through the indirect call edge.
+# SYM: 0000000000000000 {{.*}} lds_match
+
+## Kernel's LDS size should only include lds_match (128 bytes), not lds_nomatch.
+# META: .group_segment_fixed_size: 128
+
+#--- tu1.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	target_match                    ; -- Begin function target_match
+	.p2align	6
+	.type	target_match, at function
+target_match:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	target_match, .Lfunc_end0-target_match
+	.set .Ltarget_match.num_vgpr, 2
+	.set .Ltarget_match.num_agpr, 0
+	.set .Ltarget_match.numbered_sgpr, 32
+	.set .Ltarget_match.num_named_barrier, 0
+	.set .Ltarget_match.private_seg_size, 0
+	.set .Ltarget_match.uses_vcc, 0
+	.set .Ltarget_match.uses_flat_scratch, 0
+	.set .Ltarget_match.has_dyn_sized_stack, 0
+	.set .Ltarget_match.has_recursion, 0
+	.set .Ltarget_match.has_indirect_call, 0
+	.text
+	.globl	target_nomatch                  ; -- Begin function target_nomatch
+	.p2align	6
+	.type	target_nomatch, at function
+target_nomatch:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end1:
+	.size	target_nomatch, .Lfunc_end1-target_nomatch
+	.set .Ltarget_nomatch.num_vgpr, 3
+	.set .Ltarget_nomatch.num_agpr, 0
+	.set .Ltarget_nomatch.numbered_sgpr, 32
+	.set .Ltarget_nomatch.num_named_barrier, 0
+	.set .Ltarget_nomatch.private_seg_size, 0
+	.set .Ltarget_nomatch.uses_vcc, 0
+	.set .Ltarget_nomatch.uses_flat_scratch, 0
+	.set .Ltarget_nomatch.has_dyn_sized_stack, 0
+	.set .Ltarget_nomatch.has_recursion, 0
+	.set .Ltarget_nomatch.has_indirect_call, 0
+	.text
+	.globl	caller                          ; -- Begin function caller
+	.p2align	6
+	.type	caller, at function
+caller:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end2:
+	.size	caller, .Lfunc_end2-caller
+	.set .Lcaller.num_vgpr, 41
+	.set .Lcaller.num_agpr, 0
+	.set .Lcaller.numbered_sgpr, 66
+	.set .Lcaller.num_named_barrier, 0
+	.set .Lcaller.private_seg_size, 16
+	.set .Lcaller.uses_vcc, 1
+	.set .Lcaller.uses_flat_scratch, 1
+	.set .Lcaller.has_dyn_sized_stack, 1
+	.set .Lcaller.has_recursion, 1
+	.set .Lcaller.has_indirect_call, 1
+	.text
+	.globl	addr_taker                      ; -- Begin function addr_taker
+	.p2align	6
+	.type	addr_taker, at function
+addr_taker:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end3:
+	.size	addr_taker, .Lfunc_end3-addr_taker
+	.set .Laddr_taker.num_vgpr, 2
+	.set .Laddr_taker.num_agpr, 0
+	.set .Laddr_taker.numbered_sgpr, 33
+	.set .Laddr_taker.num_named_barrier, 0
+	.set .Laddr_taker.private_seg_size, 16
+	.set .Laddr_taker.uses_vcc, 0
+	.set .Laddr_taker.uses_flat_scratch, 0
+	.set .Laddr_taker.has_dyn_sized_stack, 0
+	.set .Laddr_taker.has_recursion, 0
+	.set .Laddr_taker.has_indirect_call, 0
+	.text
+	.globl	my_kernel                       ; -- Begin function my_kernel
+	.p2align	8
+	.type	my_kernel, at function
+my_kernel:
+	s_endpgm
+.Lfunc_end4:
+	.size	my_kernel, .Lfunc_end4-my_kernel
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	my_kernel.kd
+	.type	my_kernel.kd, at object
+	.size	my_kernel.kd, 64
+	.protected	my_kernel
+my_kernel.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	my_kernel at rel64-my_kernel.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469514
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Lmy_kernel.num_vgpr, 42
+	.set .Lmy_kernel.num_agpr, 0
+	.set .Lmy_kernel.numbered_sgpr, 85
+	.set .Lmy_kernel.num_named_barrier, 0
+	.set .Lmy_kernel.private_seg_size, 0
+	.set .Lmy_kernel.uses_vcc, 1
+	.set .Lmy_kernel.uses_flat_scratch, 1
+	.set .Lmy_kernel.has_dyn_sized_stack, 0
+	.set .Lmy_kernel.has_recursion, 1
+	.set .Lmy_kernel.has_indirect_call, 0
+	.amdgpu_info target_match
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_use lds_match
+		.amdgpu_typeid "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info target_nomatch
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 3
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_use lds_nomatch
+		.amdgpu_typeid "iii"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info caller
+		.amdgpu_flags 7
+		.amdgpu_num_vgpr 41
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 66
+		.amdgpu_private_segment_size 16
+		.amdgpu_indirect_call "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info addr_taker
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 16
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info my_kernel
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 42
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 85
+		.amdgpu_private_segment_size 0
+		.amdgpu_call caller
+		.amdgpu_call addr_taker
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 41
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 66
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	lds_match
+	.amdgpu_lds lds_match, 128, 16
+	.globl	lds_nomatch
+	.amdgpu_lds lds_nomatch, 256, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           my_kernel
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         my_kernel.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-indirect-resource.s b/lld/test/ELF/amdgpu-lds-link-time-indirect-resource.s
new file mode 100644
index 0000000000000..77b1efe8c4d26
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-indirect-resource.s
@@ -0,0 +1,176 @@
+# REQUIRES: amdgpu
+
+## Test that resource usage (VGPR/SGPR/scratch) propagates correctly through
+## indirect call edges. The kernel should pick up the resource requirements
+## of the indirectly-called function.
+##
+## Call graph: kern -> caller --(indirect)--> heavy_func
+## heavy_func uses significant VGPRs (via inline asm) and scratch.
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu1.s -o %t/tu1.o
+# RUN: ld.lld %t/tu1.o -o %t/out
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+## The kernel should have non-trivial resource usage propagated from heavy_func.
+## Check that private_segment_fixed_size is non-zero (scratch from heavy_func).
+# META: .private_segment_fixed_size:
+# META-NOT: .private_segment_fixed_size: 0
+
+#--- tu1.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	heavy_func                      ; -- Begin function heavy_func
+	.p2align	6
+	.type	heavy_func, at function
+heavy_func:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	heavy_func, .Lfunc_end0-heavy_func
+	.set .Lheavy_func.num_vgpr, 2
+	.set .Lheavy_func.num_agpr, 0
+	.set .Lheavy_func.numbered_sgpr, 33
+	.set .Lheavy_func.num_named_barrier, 0
+	.set .Lheavy_func.private_seg_size, 260
+	.set .Lheavy_func.uses_vcc, 0
+	.set .Lheavy_func.uses_flat_scratch, 0
+	.set .Lheavy_func.has_dyn_sized_stack, 0
+	.set .Lheavy_func.has_recursion, 0
+	.set .Lheavy_func.has_indirect_call, 0
+	.text
+	.globl	caller                          ; -- Begin function caller
+	.p2align	6
+	.type	caller, at function
+caller:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end1:
+	.size	caller, .Lfunc_end1-caller
+	.set .Lcaller.num_vgpr, 41
+	.set .Lcaller.num_agpr, 0
+	.set .Lcaller.numbered_sgpr, 66
+	.set .Lcaller.num_named_barrier, 0
+	.set .Lcaller.private_seg_size, 16
+	.set .Lcaller.uses_vcc, 1
+	.set .Lcaller.uses_flat_scratch, 1
+	.set .Lcaller.has_dyn_sized_stack, 1
+	.set .Lcaller.has_recursion, 1
+	.set .Lcaller.has_indirect_call, 1
+	.text
+	.globl	kern                            ; -- Begin function kern
+	.p2align	8
+	.type	kern, at function
+kern:
+	s_endpgm
+.Lfunc_end2:
+	.size	kern, .Lfunc_end2-kern
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kern.kd
+	.type	kern.kd, at object
+	.size	kern.kd, 64
+	.protected	kern
+kern.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	kern at rel64-kern.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Lkern.num_vgpr, 32
+	.set .Lkern.num_agpr, 0
+	.set .Lkern.numbered_sgpr, 33
+	.set .Lkern.num_named_barrier, 0
+	.set .Lkern.private_seg_size, 0
+	.set .Lkern.uses_vcc, 1
+	.set .Lkern.uses_flat_scratch, 1
+	.set .Lkern.has_dyn_sized_stack, 0
+	.set .Lkern.has_recursion, 1
+	.set .Lkern.has_indirect_call, 0
+	.amdgpu_info heavy_func
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 260
+		.amdgpu_typeid "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info caller
+		.amdgpu_flags 7
+		.amdgpu_num_vgpr 41
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 66
+		.amdgpu_private_segment_size 16
+		.amdgpu_indirect_call "vi"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info kern
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_call caller
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 41
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 66
+	.set amdgpu.max_num_named_barrier, 0
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           kern
+    .private_segment_fixed_size: 0
+    .sgpr_count:     39
+    .sgpr_spill_count: 0
+    .symbol:         kern.kd
+    .uses_dynamic_stack: true
+    .vgpr_count:     32
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-instructions.s b/lld/test/ELF/amdgpu-lds-link-time-instructions.s
new file mode 100644
index 0000000000000..c17dfce591f95
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-instructions.s
@@ -0,0 +1,164 @@
+# REQUIRES: amdgpu
+
+## End-to-end verification for link-time LDS symbol resolution. This test is
+## the focused home for the full object-to-linked-binary flow:
+##   1) Pre-link: .amdgpu_lds directives produce SHN_AMDGPU_LDS symbols.
+##   2) Pre-link: object files contain R_AMDGPU_ABS32_LO relocations and
+##      placeholder 0 literals in the load-address instructions.
+##   3) Post-link: LDS symbols become absolute symbols with resolved offsets.
+##   4) Post-link: the linker patches instructions with those offsets, and the
+##      ds_read/ds_write instructions remain intact.
+##
+## LDS layout after linking (alignment desc, size desc):
+##   lds_arr  (align=16, size=64) -> offset 0x00
+##   lds_buf  (align=4,  size=32) -> offset 0x40
+##
+## TU a.s: kernel that writes lds_arr[idx] = 42 and reads lds_buf[idx].
+## TU b.s: kernel that writes lds_buf[idx] = 99.
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=amdgcn-amd-amdhsa -mcpu=gfx900 %t/a.s -o %t/a.o
+# RUN: llvm-mc -filetype=obj -triple=amdgcn-amd-amdhsa -mcpu=gfx900 %t/b.s -o %t/b.o
+
+## Pre-link: LDS symbols and relocations in the object files.
+# RUN: llvm-readobj --syms %t/a.o | FileCheck %s --check-prefix=OBJ-A
+# RUN: llvm-readobj --syms %t/b.o | FileCheck %s --check-prefix=OBJ-B
+# RUN: llvm-objdump -d -r %t/a.o | FileCheck %s --check-prefix=PRE-A
+# RUN: llvm-objdump -d -r %t/b.o | FileCheck %s --check-prefix=PRE-B
+
+## Link.
+# RUN: ld.lld %t/a.o %t/b.o -o %t/out
+
+## Post-link: absolute LDS symbols and resolved instructions in the linked binary.
+# RUN: llvm-readobj --syms %t/out | FileCheck %s --check-prefix=LINKED
+# RUN: llvm-objdump -d %t/out | FileCheck %s --check-prefix=POST
+
+## === Pre-link LDS symbols ===
+
+# OBJ-A:      Symbol {
+# OBJ-A:        Name: lds_arr
+# OBJ-A-NEXT:   Value: 0x10
+# OBJ-A-NEXT:   Size: 64
+# OBJ-A-NEXT:   Binding: Global
+# OBJ-A-NEXT:   Type: Object
+# OBJ-A:        Section: Processor Specific (0xFF00)
+# OBJ-A-NEXT: }
+
+# OBJ-B:      Symbol {
+# OBJ-B:        Name: lds_buf
+# OBJ-B-NEXT:   Value: 0x4
+# OBJ-B-NEXT:   Size: 32
+# OBJ-B-NEXT:   Binding: Global
+# OBJ-B-NEXT:   Type: Object
+# OBJ-B:        Section: Processor Specific (0xFF00)
+# OBJ-B-NEXT: }
+
+## === Pre-link TU a: two LDS references (lds_arr and lds_buf) ===
+
+# PRE-A-LABEL: <use_lds_a>:
+## Load base of lds_arr — placeholder 0 with relocation.
+# PRE-A:      s_mov_b32 s0, lit(0x0)
+# PRE-A-NEXT:   {{.*}} R_AMDGPU_ABS32_LO lds_arr
+## Move base to VGPR and store to LDS.
+# PRE-A:      v_mov_b32_e32 v1, s0
+# PRE-A:      ds_write_b8 v1, v2
+
+## Load base of lds_buf — placeholder 0 with relocation.
+# PRE-A:      s_mov_b32 s1, lit(0x0)
+# PRE-A-NEXT:   {{.*}} R_AMDGPU_ABS32_LO lds_buf
+## Move base to VGPR and read from LDS.
+# PRE-A:      v_mov_b32_e32 v0, s1
+# PRE-A:      ds_read_u8 v0, v0
+
+## === Pre-link TU b: one LDS reference (lds_buf) ===
+
+# PRE-B-LABEL: <use_lds_b>:
+## Load base of lds_buf — placeholder 0 with relocation.
+# PRE-B:      s_mov_b32 s0, lit(0x0)
+# PRE-B-NEXT:   {{.*}} R_AMDGPU_ABS32_LO lds_buf
+## Move base to VGPR and store to LDS.
+# PRE-B:      v_mov_b32_e32 v1, s0
+# PRE-B:      ds_write_b8 v1, v2
+
+## === Post-link: relocated instructions resolved ===
+
+# LINKED:      Symbol {
+# LINKED:        Name: lds_arr
+# LINKED-NEXT:   Value: 0x0
+# LINKED-NEXT:   Size: 64
+# LINKED-NEXT:   Binding: Global
+# LINKED-NEXT:   Type: None
+# LINKED:        Section: Absolute
+# LINKED-NEXT: }
+
+# LINKED:      Symbol {
+# LINKED:        Name: lds_buf
+# LINKED-NEXT:   Value: 0x40
+# LINKED-NEXT:   Size: 32
+# LINKED-NEXT:   Binding: Global
+# LINKED-NEXT:   Type: None
+# LINKED:        Section: Absolute
+# LINKED-NEXT: }
+
+## lds_arr at offset 0x00 — s_mov_b32 resolved to 0.
+# POST-LABEL: <use_lds_a>:
+# POST:      s_mov_b32 s0, lit(0x0)
+# POST:      v_mov_b32_e32 v1, s0
+# POST:      ds_write_b8 v1, v2
+## lds_buf at offset 0x40 — s_mov_b32 patched to 0x40 (literal encoding).
+# POST:      s_mov_b32 s1, lit(0x40)
+# POST:      v_mov_b32_e32 v0, s1
+# POST:      ds_read_u8 v0, v0
+
+## In use_lds_b, lds_buf also resolved to 0x40 (same cross-TU symbol).
+# POST-LABEL: <use_lds_b>:
+# POST:      s_mov_b32 s0, lit(0x40)
+# POST:      v_mov_b32_e32 v1, s0
+# POST:      ds_write_b8 v1, v2
+
+#--- a.s
+	.text
+	.globl use_lds_a
+	.p2align 8
+	.type use_lds_a, at function
+use_lds_a:
+	; Load base address of lds_arr (relocation target).
+	s_mov_b32 s0, lds_arr at abs32@lo
+	; Move base to VGPR.
+	v_mov_b32_e32 v1, s0
+	; Store i8 42 to lds_arr[base].
+	v_mov_b32_e32 v2, 42
+	ds_write_b8 v1, v2
+
+	; Load base address of lds_buf (relocation target).
+	s_mov_b32 s1, lds_buf at abs32@lo
+	; Move base to VGPR.
+	v_mov_b32_e32 v0, s1
+	; Load i8 from lds_buf[base].
+	ds_read_u8 v0, v0
+	s_endpgm
+.Luse_lds_a_end:
+	.size use_lds_a, .Luse_lds_a_end-use_lds_a
+
+	.globl lds_arr
+	.amdgpu_lds lds_arr, 64, 16
+
+#--- b.s
+	.text
+	.globl use_lds_b
+	.p2align 8
+	.type use_lds_b, at function
+use_lds_b:
+	; Load base address of lds_buf (relocation target).
+	s_mov_b32 s0, lds_buf at abs32@lo
+	; Move base to VGPR.
+	v_mov_b32_e32 v1, s0
+	; Store i8 99 to lds_buf[base].
+	v_mov_b32_e32 v2, 99
+	ds_write_b8 v1, v2
+	s_endpgm
+.Luse_lds_b_end:
+	.size use_lds_b, .Luse_lds_b_end-use_lds_b
+
+	.globl lds_buf
+	.amdgpu_lds lds_buf, 32, 4
diff --git a/lld/test/ELF/amdgpu-lds-link-time-layout.s b/lld/test/ELF/amdgpu-lds-link-time-layout.s
new file mode 100644
index 0000000000000..36c32555f15ec
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-layout.s
@@ -0,0 +1,119 @@
+# REQUIRES: amdgpu
+
+## Comprehensive test for the LDS layout algorithm. The linker sorts
+## SHN_AMDGPU_LDS symbols by alignment (desc), size (desc), then name (asc)
+## for deterministic output, and inserts padding for alignment.
+##
+## Symbols (deliberately supplied in a scrambled order across TUs):
+##   lds_a16_s64   align=16 size=64   — tier 1 (highest alignment)
+##   lds_a16_s32   align=16 size=32   — tier 1, smaller (size tiebreaker)
+##   lds_a8_s20    align=8  size=20   — tier 2
+##   lds_a4_s12_x  align=4  size=12   — tier 3 (name tiebreaker with y)
+##   lds_a4_s12_y  align=4  size=12   — tier 3, same align+size, name > x
+##   lds_a1_s3     align=1  size=3    — tier 4 (lowest alignment)
+##
+## Expected sorted order (alignment desc, size desc, name asc):
+##   lds_a16_s64   align=16 size=64  -> offset 0x00  (0)
+##   lds_a16_s32   align=16 size=32  -> offset 0x40  (64)
+##   lds_a8_s20    align=8  size=20  -> offset 0x60  (96, 64+32=96 already 8-aligned)
+##   lds_a4_s12_x  align=4  size=12  -> offset 0x74  (116, 96+20=116 already 4-aligned)
+##   lds_a4_s12_y  align=4  size=12  -> offset 0x80  (128, 116+12=128 already 4-aligned)
+##   lds_a1_s3     align=1  size=3   -> offset 0x8C  (140, 128+12=140 1-aligned trivially)
+##
+## Total: 143 bytes.
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=amdgcn-amd-amdhsa -mcpu=gfx900 %t/tu1.s -o %t/tu1.o
+# RUN: llvm-mc -filetype=obj -triple=amdgcn-amd-amdhsa -mcpu=gfx900 %t/tu2.s -o %t/tu2.o
+# RUN: llvm-mc -filetype=obj -triple=amdgcn-amd-amdhsa -mcpu=gfx900 %t/tu3.s -o %t/tu3.o
+# RUN: ld.lld %t/tu1.o %t/tu2.o %t/tu3.o -o %t/out
+# RUN: llvm-readobj --syms %t/out | FileCheck %s
+
+## Symbols appear in file-definition order after linking (tu1, tu2, tu3).
+## Verify each symbol's assigned offset matches the layout algorithm:
+##   sort by alignment desc, size desc, name asc.
+
+## From tu1: lds_a4_s12_y (align=4, size=12) -> offset 0x80
+# CHECK:      Name: lds_a4_s12_y
+# CHECK-NEXT: Value: 0x80
+# CHECK-NEXT: Size: 12
+
+## From tu1: lds_a16_s64 (align=16, size=64) -> offset 0x0
+# CHECK:      Name: lds_a16_s64
+# CHECK-NEXT: Value: 0x0
+# CHECK-NEXT: Size: 64
+
+## From tu2: lds_a1_s3 (align=1, size=3) -> offset 0x8C
+# CHECK:      Name: lds_a1_s3
+# CHECK-NEXT: Value: 0x8C
+# CHECK-NEXT: Size: 3
+
+## From tu2: lds_a8_s20 (align=8, size=20) -> offset 0x60
+# CHECK:      Name: lds_a8_s20
+# CHECK-NEXT: Value: 0x60
+# CHECK-NEXT: Size: 20
+
+## From tu3: lds_a16_s32 (align=16, size=32) -> offset 0x40
+# CHECK:      Name: lds_a16_s32
+# CHECK-NEXT: Value: 0x40
+# CHECK-NEXT: Size: 32
+
+## From tu3: lds_a4_s12_x (align=4, size=12) -> offset 0x74
+# CHECK:      Name: lds_a4_s12_x
+# CHECK-NEXT: Value: 0x74
+# CHECK-NEXT: Size: 12
+
+#--- tu1.s
+## Deliberately interleave symbols from different tiers.
+	.text
+	.globl f1
+	.p2align 8
+	.type f1, at function
+f1:
+	s_mov_b32 s0, lds_a4_s12_y at abs32@lo
+	s_mov_b32 s1, lds_a16_s64 at abs32@lo
+	s_endpgm
+.Lf1_end:
+	.size f1, .Lf1_end-f1
+
+	.globl lds_a4_s12_y
+	.amdgpu_lds lds_a4_s12_y, 12, 4
+
+	.globl lds_a16_s64
+	.amdgpu_lds lds_a16_s64, 64, 16
+
+#--- tu2.s
+	.text
+	.globl f2
+	.p2align 8
+	.type f2, at function
+f2:
+	s_mov_b32 s0, lds_a1_s3 at abs32@lo
+	s_mov_b32 s1, lds_a8_s20 at abs32@lo
+	s_endpgm
+.Lf2_end:
+	.size f2, .Lf2_end-f2
+
+	.globl lds_a1_s3
+	.amdgpu_lds lds_a1_s3, 3, 1
+
+	.globl lds_a8_s20
+	.amdgpu_lds lds_a8_s20, 20, 8
+
+#--- tu3.s
+	.text
+	.globl f3
+	.p2align 8
+	.type f3, at function
+f3:
+	s_mov_b32 s0, lds_a16_s32 at abs32@lo
+	s_mov_b32 s1, lds_a4_s12_x at abs32@lo
+	s_endpgm
+.Lf3_end:
+	.size f3, .Lf3_end-f3
+
+	.globl lds_a16_s32
+	.amdgpu_lds lds_a16_s32, 32, 16
+
+	.globl lds_a4_s12_x
+	.amdgpu_lds lds_a4_s12_x, 12, 4
diff --git a/lld/test/ELF/amdgpu-lds-link-time-named-barrier.s b/lld/test/ELF/amdgpu-lds-link-time-named-barrier.s
new file mode 100644
index 0000000000000..1ccd99445edbd
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-named-barrier.s
@@ -0,0 +1,260 @@
+# REQUIRES: amdgpu
+
+## Test that lld patches compute_pgm_rsrc3.NAMED_BAR_CNT with the cross-TU
+## propagated named-barrier count for GFX1250.
+##
+## TU A: kern_a uses 1 named barrier, calls external helper.
+## TU B: helper uses 5 named barriers, kern_b also uses 5.
+## After linking, kern_a gets max(1, 5) = 5 barriers -> NamedBarCnt = ceil(5/4) = 2.
+## NAMED_BAR_CNT occupies bits [14:16] of compute_pgm_rsrc3.
+## Expected: 2 << 14 = 0x8000 for both kern_a and kern_b.
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx1250 -filetype=obj %t/a.s -o %t/a.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx1250 -filetype=obj %t/b.s -o %t/b.o
+# RUN: ld.lld %t/a.o %t/b.o -o %t/out
+
+## .rodata contains two 64-byte kernel descriptors. Check compute_pgm_rsrc3 at
+## offset 44 from each KD start. NAMED_BAR_CNT=2 is encoded as 0x00008000,
+## printed below as little-endian bytes 00800000.
+# RUN: llvm-objdump -s -j .rodata %t/out | FileCheck %s --check-prefix=KD
+
+# KD: {{[0-9a-f]+}} 00000000 00000000 00000000 00800000
+# KD: {{[0-9a-f]+}} 00000000 00000000 00000000 00800000
+
+#--- a.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx1250"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	kern_a                          ; -- Begin function kern_a
+	.p2align	8
+	.type	kern_a, at function
+kern_a:
+	s_endpgm
+.Lfunc_end0:
+	.size	kern_a, .Lfunc_end0-kern_a
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kern_a.kd
+	.type	kern_a.kd, at object
+	.size	kern_a.kd, 64
+	.protected	kern_a
+kern_a.kd:
+	.long	0
+	.long	0
+	.long	256
+	.long	0
+	.quad	kern_a at rel64-kern_a.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	3222208513
+	.long	5008
+	.short	1054
+	.short	0
+	.long	0
+	.text
+	.set .Lkern_a.num_vgpr, 32
+	.set .Lkern_a.num_agpr, 0
+	.set .Lkern_a.numbered_sgpr, 33
+	.set .Lkern_a.num_named_barrier, 0
+	.set .Lkern_a.private_seg_size, 0
+	.set .Lkern_a.uses_vcc, 1
+	.set .Lkern_a.uses_flat_scratch, 0
+	.set .Lkern_a.has_dyn_sized_stack, 0
+	.set .Lkern_a.has_recursion, 0
+	.set .Lkern_a.has_indirect_call, 0
+	.text
+	.amdgpu_info kern_a
+		.amdgpu_flags 1
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_use __amdgpu_named_barrier.bar_a.4afea3fa75d4f11a11e1e1e3237d94b2
+		.amdgpu_call helper
+		.amdgpu_occupancy 8
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.text
+	.globl	__amdgpu_named_barrier.bar_a.4afea3fa75d4f11a11e1e1e3237d94b2
+	.amdgpu_lds __amdgpu_named_barrier.bar_a.4afea3fa75d4f11a11e1e1e3237d94b2, 16, 4
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 256
+    .max_flat_workgroup_size: 1024
+    .name:           kern_a
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         kern_a.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 32
+amdhsa.target:   amdgcn-amd-amdhsa--gfx1250
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- b.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx1250"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	helper                          ; -- Begin function helper
+	.p2align	7
+	.type	helper, at function
+helper:
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	helper, .Lfunc_end0-helper
+	.set .Lhelper.num_vgpr, 0
+	.set .Lhelper.num_agpr, 0
+	.set .Lhelper.numbered_sgpr, 32
+	.set .Lhelper.num_named_barrier, 0
+	.set .Lhelper.private_seg_size, 0
+	.set .Lhelper.uses_vcc, 0
+	.set .Lhelper.uses_flat_scratch, 0
+	.set .Lhelper.has_dyn_sized_stack, 0
+	.set .Lhelper.has_recursion, 0
+	.set .Lhelper.has_indirect_call, 0
+	.text
+	.globl	kern_b                          ; -- Begin function kern_b
+	.p2align	8
+	.type	kern_b, at function
+kern_b:
+	s_endpgm
+.Lfunc_end1:
+	.size	kern_b, .Lfunc_end1-kern_b
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kern_b.kd
+	.type	kern_b.kd, at object
+	.size	kern_b.kd, 64
+	.protected	kern_b
+kern_b.kd:
+	.long	0
+	.long	0
+	.long	256
+	.long	0
+	.quad	kern_b at rel64-kern_b.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	3222208512
+	.long	5008
+	.short	1054
+	.short	0
+	.long	0
+	.text
+	.set .Lkern_b.num_vgpr, 0
+	.set .Lkern_b.num_agpr, 0
+	.set .Lkern_b.numbered_sgpr, 1
+	.set .Lkern_b.num_named_barrier, 0
+	.set .Lkern_b.private_seg_size, 0
+	.set .Lkern_b.uses_vcc, 0
+	.set .Lkern_b.uses_flat_scratch, 0
+	.set .Lkern_b.has_dyn_sized_stack, 0
+	.set .Lkern_b.has_recursion, 0
+	.set .Lkern_b.has_indirect_call, 0
+	.text
+	.amdgpu_info helper
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 0
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_use __amdgpu_named_barrier.bar_b.36a90f902d5ba5557b314d56b5259233
+		.amdgpu_occupancy 8
+	.end_amdgpu_info
+
+	.amdgpu_info kern_b
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 0
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use __amdgpu_named_barrier.bar_b.36a90f902d5ba5557b314d56b5259233
+		.amdgpu_occupancy 8
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 32
+	.set amdgpu.max_num_named_barrier, 0
+	.text
+	.globl	__amdgpu_named_barrier.bar_b.36a90f902d5ba5557b314d56b5259233
+	.amdgpu_lds __amdgpu_named_barrier.bar_b.36a90f902d5ba5557b314d56b5259233, 80, 4
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 256
+    .max_flat_workgroup_size: 1024
+    .name:           kern_b
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         kern_b.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 32
+amdhsa.target:   amdgcn-amd-amdhsa--gfx1250
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-ordering-complex.s b/lld/test/ELF/amdgpu-lds-link-time-ordering-complex.s
new file mode 100644
index 0000000000000..ea0cac388bb6b
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-ordering-complex.s
@@ -0,0 +1,402 @@
+# REQUIRES: amdgpu
+
+## Test shared-tier ordering with 4 kernels and 4 shared-tier variables with
+## a complex overlapping usage pattern.
+##
+## Variables (all external linkage -> global-scope -> shared tier):
+##   V1: 4 bytes align 4, used by K1, K2, K3, K4 (4 users)
+##   V2: 8 bytes align 4, used by K1, K2, K3     (3 users)
+##   V3: 16 bytes align 4->16*, used by K1, K2   (2 users)
+##   V4: 32 bytes align 4->16*, used by K1, K3   (2 users)
+##   (* superAlignLDSGlobals bumps alignment to 16 for vars >= 16 bytes)
+##
+## Per-kernel frontier order:
+##   V1 and V2 are placed first by use count. V4 and V3 have equal use counts,
+##   so V4 is placed before V3 by the size/alignment tie-breaker.
+##
+## Layout: V1(4,a4)@0x00, V2(8,a8)@0x08, V4(32,a16)@0x10, V3(16,a16)@0x30
+##   (superAlignLDSGlobals also bumps V2 to align 8)
+##
+## Per-kernel sizes:
+##   K1: all -> max(4, 16, 32, 64) = 64 = 0x40
+##   K2: V1,V2,V3 -> max(4, 16, 64) = 64 = 0x40
+##   K3: V1,V2,V4 -> max(4, 16, 48) = 48 = 0x30
+##   K4: V1 -> 4 = 0x04
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu1.s -o %t/tu1.o
+# RUN: ld.lld %t/tu1.o -o %t/out
+# RUN: llvm-readelf -s %t/out | FileCheck %s --check-prefix=SYM
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+## V1 (4 users) at lowest offset
+# SYM-DAG: 0000000000000000 {{.*}} V1
+## V2 (was 3 users, placed early after dynamic updates; aligned to 8)
+# SYM-DAG: 0000000000000008 {{.*}} V2
+## V4 (2 users, largest) before V3
+# SYM-DAG: 0000000000000010 {{.*}} V4
+# SYM-DAG: 0000000000000030 {{.*}} V3
+
+## K1 uses all: size = 64
+# META-DAG: .group_segment_fixed_size: 64
+## K2 uses V1,V2,V3: size = 64
+# META-DAG: .group_segment_fixed_size: 64
+## K3 uses V1,V2,V4: size = 48
+# META-DAG: .group_segment_fixed_size: 48
+## K4 uses only V1: minimal size = 4
+# META-DAG: .group_segment_fixed_size: 4
+
+#--- tu1.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	K1                              ; -- Begin function K1
+	.p2align	8
+	.type	K1, at function
+K1:
+	s_endpgm
+.Lfunc_end0:
+	.size	K1, .Lfunc_end0-K1
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K1.kd
+	.type	K1.kd, at object
+	.size	K1.kd, 64
+	.protected	K1
+K1.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K1 at rel64-K1.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK1.num_vgpr, 3
+	.set .LK1.num_agpr, 0
+	.set .LK1.numbered_sgpr, 10
+	.set .LK1.num_named_barrier, 0
+	.set .LK1.private_seg_size, 0
+	.set .LK1.uses_vcc, 0
+	.set .LK1.uses_flat_scratch, 0
+	.set .LK1.has_dyn_sized_stack, 0
+	.set .LK1.has_recursion, 0
+	.set .LK1.has_indirect_call, 0
+	.text
+	.globl	K2                              ; -- Begin function K2
+	.p2align	8
+	.type	K2, at function
+K2:
+	s_endpgm
+.Lfunc_end1:
+	.size	K2, .Lfunc_end1-K2
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K2.kd
+	.type	K2.kd, at object
+	.size	K2.kd, 64
+	.protected	K2
+K2.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K2 at rel64-K2.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK2.num_vgpr, 3
+	.set .LK2.num_agpr, 0
+	.set .LK2.numbered_sgpr, 10
+	.set .LK2.num_named_barrier, 0
+	.set .LK2.private_seg_size, 0
+	.set .LK2.uses_vcc, 0
+	.set .LK2.uses_flat_scratch, 0
+	.set .LK2.has_dyn_sized_stack, 0
+	.set .LK2.has_recursion, 0
+	.set .LK2.has_indirect_call, 0
+	.text
+	.globl	K3                              ; -- Begin function K3
+	.p2align	8
+	.type	K3, at function
+K3:
+	s_endpgm
+.Lfunc_end2:
+	.size	K3, .Lfunc_end2-K3
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K3.kd
+	.type	K3.kd, at object
+	.size	K3.kd, 64
+	.protected	K3
+K3.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K3 at rel64-K3.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK3.num_vgpr, 3
+	.set .LK3.num_agpr, 0
+	.set .LK3.numbered_sgpr, 10
+	.set .LK3.num_named_barrier, 0
+	.set .LK3.private_seg_size, 0
+	.set .LK3.uses_vcc, 0
+	.set .LK3.uses_flat_scratch, 0
+	.set .LK3.has_dyn_sized_stack, 0
+	.set .LK3.has_recursion, 0
+	.set .LK3.has_indirect_call, 0
+	.text
+	.globl	K4                              ; -- Begin function K4
+	.p2align	8
+	.type	K4, at function
+K4:
+	s_endpgm
+.Lfunc_end3:
+	.size	K4, .Lfunc_end3-K4
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K4.kd
+	.type	K4.kd, at object
+	.size	K4.kd, 64
+	.protected	K4
+K4.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K4 at rel64-K4.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468800
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK4.num_vgpr, 2
+	.set .LK4.num_agpr, 0
+	.set .LK4.numbered_sgpr, 0
+	.set .LK4.num_named_barrier, 0
+	.set .LK4.private_seg_size, 0
+	.set .LK4.uses_vcc, 0
+	.set .LK4.uses_flat_scratch, 0
+	.set .LK4.has_dyn_sized_stack, 0
+	.set .LK4.has_recursion, 0
+	.set .LK4.has_indirect_call, 0
+	.amdgpu_info K1
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 3
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use V3
+		.amdgpu_use V4
+		.amdgpu_use V1
+		.amdgpu_use V2
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K2
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 3
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use V3
+		.amdgpu_use V1
+		.amdgpu_use V2
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K3
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 3
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use V4
+		.amdgpu_use V1
+		.amdgpu_use V2
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K4
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 0
+		.amdgpu_private_segment_size 0
+		.amdgpu_use V1
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	V1
+	.amdgpu_lds V1, 4, 4
+	.globl	V2
+	.amdgpu_lds V2, 8, 8
+	.globl	V3
+	.amdgpu_lds V3, 16, 16
+	.globl	V4
+	.amdgpu_lds V4, 32, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K1
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K1.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K2
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K2.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K3
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K3.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K4
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K4.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-ordering-frontier.s b/lld/test/ELF/amdgpu-lds-link-time-ordering-frontier.s
new file mode 100644
index 0000000000000..38309bae811d1
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-ordering-frontier.s
@@ -0,0 +1,112 @@
+# REQUIRES: amdgpu
+
+## Test per-kernel LDS frontiers. P is allocated first because it has the most
+## users. L can still start at offset 0 because none of P's users use L. M then
+## lands at the max frontier of K1 and K2.
+##
+##   K1: P, M
+##   K2: M, L
+##   K3: L
+##   K4: P
+##   K5: P
+
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %s -o %t.o
+# RUN: ld.lld %t.o -o %t
+# RUN: llvm-readelf -s %t | FileCheck %s --check-prefix=SYM
+
+# SYM-DAG: 0000000000000000 {{.*}} P
+# SYM-DAG: 0000000000000000 {{.*}} L
+# SYM-DAG: 0000000000000010 {{.*}} M
+
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+
+	.macro kernel name
+	.text
+	.globl	\name
+	.p2align	8
+	.type	\name, at function
+\name:
+	s_endpgm
+.L\name\()_end:
+	.size	\name, .L\name\()_end-\name
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	\name\().kd
+	.type	\name\().kd, at object
+	.size	\name\().kd, 64
+	.protected	\name
+\name\().kd:
+	.long	0
+	.long	0
+	.long	0
+	.long	0
+	.quad	\name at rel64-\name\().kd
+	.zero	20
+	.long	0
+	.long	0
+	.long	0
+	.short	0
+	.short	0
+	.long	0
+	.endm
+
+	kernel K1
+	kernel K2
+	kernel K3
+	kernel K4
+	kernel K5
+
+	.amdgpu_info K1
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use P
+		.amdgpu_use M
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K2
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use M
+		.amdgpu_use L
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K3
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use L
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K4
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use P
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K5
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use P
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.globl	P
+	.amdgpu_lds P, 8, 8
+	.globl	L
+	.amdgpu_lds L, 16, 16
+	.globl	M
+	.amdgpu_lds M, 4, 4
diff --git a/lld/test/ELF/amdgpu-lds-link-time-ordering-multigroup.s b/lld/test/ELF/amdgpu-lds-link-time-ordering-multigroup.s
new file mode 100644
index 0000000000000..0ad2b86f1409e
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-ordering-multigroup.s
@@ -0,0 +1,505 @@
+# REQUIRES: amdgpu
+
+## Test that multiple independent groups each have their shared-tier ordering
+## optimized independently, and each group allocates from offset 0.
+##
+## Group 1 (K1, K2 share A and B):
+##   A: 8 bytes align 4, used by K1, K2, K3     (3 users)
+##   B: 16 bytes align 4->16*, used by K1, K2   (2 users)
+##   C: 32 bytes align 4->16*, used by K1, K2   (2 users)
+##
+## Group 2 (K4, K5 share D):
+##   D: 4 bytes align 4, used by K4, K5  (2 users)
+##   E: 8 bytes align 4, used by K4, K5  (2 users)
+##
+## Groups are independent (no shared LDS between them).
+##
+## Group 1 frontier order: [A, C, B] (A at 0x00, C at 0x10, B at 0x30)
+##   K3 uses only A -> size = 8
+##
+## Group 2 frontier order: both D and E have use_count=2.
+##   Tie-break by size: E(8)>D(4), so E is placed first.
+##   Result: [E, D] (E at 0x00, D at 0x08)
+##   (* superAlignLDSGlobals bumps alignment to 16 for vars >= 16 bytes,
+##      and E to align 8 since it is 8 bytes)
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu1.s -o %t/tu1.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu2.s -o %t/tu2.o
+# RUN: ld.lld %t/tu1.o %t/tu2.o -o %t/out
+# RUN: llvm-readelf -s %t/out | FileCheck %s --check-prefix=SYM
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+## Group 1: A most shared, at offset 0
+# SYM-DAG: 0000000000000000 {{.*}} A
+# SYM-DAG: 0000000000000010 {{.*}} C
+# SYM-DAG: 0000000000000030 {{.*}} B
+
+## Group 2 is independent and starts from offset 0
+# SYM-DAG: 0000000000000000 {{.*}} E
+# SYM-DAG: 0000000000000008 {{.*}} D
+
+## K3 uses only A -> 8 bytes
+# META-DAG: .group_segment_fixed_size: 8
+## K1 and K2 use all of group 1 -> 64 bytes
+# META-DAG: .group_segment_fixed_size: 64
+# META-DAG: .group_segment_fixed_size: 64
+## K4 and K5 use all of group 2 -> 12 bytes (E at 0..8, D at 8..12)
+# META-DAG: .group_segment_fixed_size: 12
+# META-DAG: .group_segment_fixed_size: 12
+
+#--- tu1.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	K1                              ; -- Begin function K1
+	.p2align	8
+	.type	K1, at function
+K1:
+	s_endpgm
+.Lfunc_end0:
+	.size	K1, .Lfunc_end0-K1
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K1.kd
+	.type	K1.kd, at object
+	.size	K1.kd, 64
+	.protected	K1
+K1.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K1 at rel64-K1.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK1.num_vgpr, 4
+	.set .LK1.num_agpr, 0
+	.set .LK1.numbered_sgpr, 10
+	.set .LK1.num_named_barrier, 0
+	.set .LK1.private_seg_size, 0
+	.set .LK1.uses_vcc, 0
+	.set .LK1.uses_flat_scratch, 0
+	.set .LK1.has_dyn_sized_stack, 0
+	.set .LK1.has_recursion, 0
+	.set .LK1.has_indirect_call, 0
+	.text
+	.globl	K2                              ; -- Begin function K2
+	.p2align	8
+	.type	K2, at function
+K2:
+	s_endpgm
+.Lfunc_end1:
+	.size	K2, .Lfunc_end1-K2
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K2.kd
+	.type	K2.kd, at object
+	.size	K2.kd, 64
+	.protected	K2
+K2.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K2 at rel64-K2.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK2.num_vgpr, 4
+	.set .LK2.num_agpr, 0
+	.set .LK2.numbered_sgpr, 10
+	.set .LK2.num_named_barrier, 0
+	.set .LK2.private_seg_size, 0
+	.set .LK2.uses_vcc, 0
+	.set .LK2.uses_flat_scratch, 0
+	.set .LK2.has_dyn_sized_stack, 0
+	.set .LK2.has_recursion, 0
+	.set .LK2.has_indirect_call, 0
+	.text
+	.globl	K3                              ; -- Begin function K3
+	.p2align	8
+	.type	K3, at function
+K3:
+	s_endpgm
+.Lfunc_end2:
+	.size	K3, .Lfunc_end2-K3
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K3.kd
+	.type	K3.kd, at object
+	.size	K3.kd, 64
+	.protected	K3
+K3.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K3 at rel64-K3.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK3.num_vgpr, 2
+	.set .LK3.num_agpr, 0
+	.set .LK3.numbered_sgpr, 10
+	.set .LK3.num_named_barrier, 0
+	.set .LK3.private_seg_size, 0
+	.set .LK3.uses_vcc, 0
+	.set .LK3.uses_flat_scratch, 0
+	.set .LK3.has_dyn_sized_stack, 0
+	.set .LK3.has_recursion, 0
+	.set .LK3.has_indirect_call, 0
+	.amdgpu_info K1
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 4
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use B
+		.amdgpu_use C
+		.amdgpu_use A
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K2
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 4
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use B
+		.amdgpu_use C
+		.amdgpu_use A
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K3
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use A
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	A
+	.amdgpu_lds A, 8, 8
+	.globl	B
+	.amdgpu_lds B, 16, 16
+	.globl	C
+	.amdgpu_lds C, 32, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K1
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K1.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K2
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K2.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K3
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K3.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- tu2.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	K4                              ; -- Begin function K4
+	.p2align	8
+	.type	K4, at function
+K4:
+	s_endpgm
+.Lfunc_end0:
+	.size	K4, .Lfunc_end0-K4
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K4.kd
+	.type	K4.kd, at object
+	.size	K4.kd, 64
+	.protected	K4
+K4.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K4 at rel64-K4.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK4.num_vgpr, 2
+	.set .LK4.num_agpr, 0
+	.set .LK4.numbered_sgpr, 10
+	.set .LK4.num_named_barrier, 0
+	.set .LK4.private_seg_size, 0
+	.set .LK4.uses_vcc, 0
+	.set .LK4.uses_flat_scratch, 0
+	.set .LK4.has_dyn_sized_stack, 0
+	.set .LK4.has_recursion, 0
+	.set .LK4.has_indirect_call, 0
+	.text
+	.globl	K5                              ; -- Begin function K5
+	.p2align	8
+	.type	K5, at function
+K5:
+	s_endpgm
+.Lfunc_end1:
+	.size	K5, .Lfunc_end1-K5
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K5.kd
+	.type	K5.kd, at object
+	.size	K5.kd, 64
+	.protected	K5
+K5.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K5 at rel64-K5.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK5.num_vgpr, 2
+	.set .LK5.num_agpr, 0
+	.set .LK5.numbered_sgpr, 10
+	.set .LK5.num_named_barrier, 0
+	.set .LK5.private_seg_size, 0
+	.set .LK5.uses_vcc, 0
+	.set .LK5.uses_flat_scratch, 0
+	.set .LK5.has_dyn_sized_stack, 0
+	.set .LK5.has_recursion, 0
+	.set .LK5.has_indirect_call, 0
+	.amdgpu_info K4
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use E
+		.amdgpu_use D
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K5
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use E
+		.amdgpu_use D
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	D
+	.amdgpu_lds D, 4, 4
+	.globl	E
+	.amdgpu_lds E, 8, 8
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K4
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K4.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K5
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K5.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-ordering-sizes.s b/lld/test/ELF/amdgpu-lds-link-time-ordering-sizes.s
new file mode 100644
index 0000000000000..b0d45f8529fdd
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-ordering-sizes.s
@@ -0,0 +1,209 @@
+# REQUIRES: amdgpu
+
+## Test size-based tie-breaking when shared-tier variables have equal use
+## counts. The per-kernel frontier allocator breaks ties by placing larger
+## variables first.
+##
+## Variables (all external linkage -> global-scope -> shared tier):
+##   big:   64 bytes align 4->16*, used by K1, K2 (2 users)
+##   small: 8 bytes align 4, used by K1, K2       (2 users)
+##   (* superAlignLDSGlobals bumps alignment to 16 for vars >= 16 bytes)
+##
+## Both have use_count=2. The tie breaks by size, so big is placed first.
+##
+## Layout: big(64,a16)@0x00, small(8,a8)@0x40
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu1.s -o %t/tu1.o
+# RUN: ld.lld %t/tu1.o -o %t/out
+# RUN: llvm-readelf -s %t/out | FileCheck %s --check-prefix=SYM
+
+## big is first, then small at the shared kernel frontier
+# SYM-DAG: 0000000000000000 {{.*}} big
+# SYM-DAG: 0000000000000040 {{.*}} small
+
+#--- tu1.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	K1                              ; -- Begin function K1
+	.p2align	8
+	.type	K1, at function
+K1:
+	s_endpgm
+.Lfunc_end0:
+	.size	K1, .Lfunc_end0-K1
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K1.kd
+	.type	K1.kd, at object
+	.size	K1.kd, 64
+	.protected	K1
+K1.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K1 at rel64-K1.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK1.num_vgpr, 4
+	.set .LK1.num_agpr, 0
+	.set .LK1.numbered_sgpr, 10
+	.set .LK1.num_named_barrier, 0
+	.set .LK1.private_seg_size, 0
+	.set .LK1.uses_vcc, 0
+	.set .LK1.uses_flat_scratch, 0
+	.set .LK1.has_dyn_sized_stack, 0
+	.set .LK1.has_recursion, 0
+	.set .LK1.has_indirect_call, 0
+	.text
+	.globl	K2                              ; -- Begin function K2
+	.p2align	8
+	.type	K2, at function
+K2:
+	s_endpgm
+.Lfunc_end1:
+	.size	K2, .Lfunc_end1-K2
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K2.kd
+	.type	K2.kd, at object
+	.size	K2.kd, 64
+	.protected	K2
+K2.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K2 at rel64-K2.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK2.num_vgpr, 4
+	.set .LK2.num_agpr, 0
+	.set .LK2.numbered_sgpr, 10
+	.set .LK2.num_named_barrier, 0
+	.set .LK2.private_seg_size, 0
+	.set .LK2.uses_vcc, 0
+	.set .LK2.uses_flat_scratch, 0
+	.set .LK2.has_dyn_sized_stack, 0
+	.set .LK2.has_recursion, 0
+	.set .LK2.has_indirect_call, 0
+	.amdgpu_info K1
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 4
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use small
+		.amdgpu_use big
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K2
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 4
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use small
+		.amdgpu_use big
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	big
+	.amdgpu_lds big, 64, 16
+	.globl	small
+	.amdgpu_lds small, 8, 8
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K1
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K1.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K2
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K2.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-ordering-tiers.s b/lld/test/ELF/amdgpu-lds-link-time-ordering-tiers.s
new file mode 100644
index 0000000000000..965b1813a5f81
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-ordering-tiers.s
@@ -0,0 +1,313 @@
+# REQUIRES: amdgpu
+
+## Test that shared-tier ordering is optimized while kernel-tier variables
+## are correctly appended after the shared tier.
+##
+## Variables:
+##   shared_a: 8 bytes align 4, external, used by K1, K2, K3  (shared, 3 users)
+##   shared_b: 32 bytes align 4->16*, external, used by K1, K2 (shared, 2 users)
+##   k1_priv:  16 bytes align 4->16*, internal, used by K1     (kernel tier)
+##   (* superAlignLDSGlobals bumps alignment to 16 for vars >= 16 bytes)
+##
+## Greedy shared tier from tail:
+##   Position 2: use_count shared_a=3, shared_b=2. Min=2 -> shared_b.
+##     K1,K2 fixed. shared_a->1.
+##   Position 1: shared_a left.
+##   Result: [shared_a, shared_b]
+##
+## Layout:
+##   Shared: shared_a(8,a4)@0x00, shared_b(32,a16)@0x10
+##   Kernel: __amdgpu_lds.K1(16,a16)@0x30
+##
+## Per-kernel sizes:
+##   K1: shared_a(0..8), shared_b(16..48), K1's struct(48..64) -> 64 = 0x40
+##   K2: shared_a(0..8), shared_b(16..48) -> 48 = 0x30
+##   K3: shared_a(0..8) -> 8 = 0x08 (zero waste!)
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu1.s -o %t/tu1.o
+# RUN: ld.lld %t/tu1.o -o %t/out
+# RUN: llvm-readelf -s %t/out | FileCheck %s --check-prefix=SYM
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+## Shared tier: shared_a first (most shared), shared_b after
+# SYM-DAG: 0000000000000000 {{.*}} shared_a
+# SYM-DAG: 0000000000000010 {{.*}} shared_b
+## Kernel tier appended after shared tier
+# SYM-DAG: 0000000000000030 {{.*}} __amdgpu_lds.K1
+
+## K3 (uses only shared_a) has minimal size
+# META-DAG: .group_segment_fixed_size: 8
+## K2 (uses shared_a + shared_b)
+# META-DAG: .group_segment_fixed_size: 48
+## K1 (uses shared_a + shared_b + k1_priv)
+# META-DAG: .group_segment_fixed_size: 64
+
+#--- tu1.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	K1                              ; -- Begin function K1
+	.p2align	8
+	.type	K1, at function
+K1:
+	s_endpgm
+.Lfunc_end0:
+	.size	K1, .Lfunc_end0-K1
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K1.kd
+	.type	K1.kd, at object
+	.size	K1.kd, 64
+	.protected	K1
+K1.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K1 at rel64-K1.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK1.num_vgpr, 4
+	.set .LK1.num_agpr, 0
+	.set .LK1.numbered_sgpr, 10
+	.set .LK1.num_named_barrier, 0
+	.set .LK1.private_seg_size, 0
+	.set .LK1.uses_vcc, 0
+	.set .LK1.uses_flat_scratch, 0
+	.set .LK1.has_dyn_sized_stack, 0
+	.set .LK1.has_recursion, 0
+	.set .LK1.has_indirect_call, 0
+	.text
+	.globl	K2                              ; -- Begin function K2
+	.p2align	8
+	.type	K2, at function
+K2:
+	s_endpgm
+.Lfunc_end1:
+	.size	K2, .Lfunc_end1-K2
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K2.kd
+	.type	K2.kd, at object
+	.size	K2.kd, 64
+	.protected	K2
+K2.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K2 at rel64-K2.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK2.num_vgpr, 4
+	.set .LK2.num_agpr, 0
+	.set .LK2.numbered_sgpr, 10
+	.set .LK2.num_named_barrier, 0
+	.set .LK2.private_seg_size, 0
+	.set .LK2.uses_vcc, 0
+	.set .LK2.uses_flat_scratch, 0
+	.set .LK2.has_dyn_sized_stack, 0
+	.set .LK2.has_recursion, 0
+	.set .LK2.has_indirect_call, 0
+	.text
+	.globl	K3                              ; -- Begin function K3
+	.p2align	8
+	.type	K3, at function
+K3:
+	s_endpgm
+.Lfunc_end2:
+	.size	K3, .Lfunc_end2-K3
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	K3.kd
+	.type	K3.kd, at object
+	.size	K3.kd, 64
+	.protected	K3
+K3.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	K3 at rel64-K3.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .LK3.num_vgpr, 2
+	.set .LK3.num_agpr, 0
+	.set .LK3.numbered_sgpr, 10
+	.set .LK3.num_named_barrier, 0
+	.set .LK3.private_seg_size, 0
+	.set .LK3.uses_vcc, 0
+	.set .LK3.uses_flat_scratch, 0
+	.set .LK3.has_dyn_sized_stack, 0
+	.set .LK3.has_recursion, 0
+	.set .LK3.has_indirect_call, 0
+	.amdgpu_info K1
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 4
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use __amdgpu_lds.K1
+		.amdgpu_use shared_b
+		.amdgpu_use shared_a
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K2
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 4
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use shared_b
+		.amdgpu_use shared_a
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K3
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use shared_a
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	shared_a
+	.amdgpu_lds shared_a, 8, 8
+	.globl	shared_b
+	.amdgpu_lds shared_b, 32, 16
+	.globl	__amdgpu_lds.K1
+	.amdgpu_lds __amdgpu_lds.K1, 16, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K1
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K1.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K2
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K2.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           K3
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         K3.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-padding.s b/lld/test/ELF/amdgpu-lds-link-time-padding.s
new file mode 100644
index 0000000000000..d8d46bf771511
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-padding.s
@@ -0,0 +1,82 @@
+# REQUIRES: amdgpu
+
+## Test that the linker inserts padding between LDS symbols when a symbol's
+## size is not a multiple of the next symbol's alignment requirement.
+##
+## Symbols:
+##   lds_big   align=16 size=20  — leaves offset at 20, not 8-aligned
+##   lds_med   align=8  size=7   — needs padding to offset 24, leaves at 31
+##   lds_small align=4  size=5   — needs padding to offset 32, leaves at 37
+##   lds_tiny  align=1  size=1   — no padding needed, offset 37
+##
+## Expected layout (alignment desc, size desc):
+##   lds_big   -> offset 0x00  (0)
+##   lds_med   -> offset 0x18  (24 = alignTo(20, 8), 4 bytes padding)
+##   lds_small -> offset 0x20  (32 = alignTo(31, 4), 1 byte padding)
+##   lds_tiny  -> offset 0x25  (37 = alignTo(37, 1), no padding)
+##
+## Total: 38 bytes. Padding: 4 bytes at [20,24) + 1 byte at [31,32).
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=amdgcn-amd-amdhsa -mcpu=gfx900 %t/a.s -o %t/a.o
+# RUN: llvm-mc -filetype=obj -triple=amdgcn-amd-amdhsa -mcpu=gfx900 %t/b.s -o %t/b.o
+# RUN: ld.lld %t/a.o %t/b.o -o %t/out
+# RUN: llvm-readobj --syms %t/out | FileCheck %s
+
+## Symbols appear in file-definition order (a.o then b.o).
+
+## From a.o: lds_tiny (align=1, size=1) -> offset 0x25 (no padding from lds_small)
+# CHECK:      Name: lds_tiny
+# CHECK-NEXT: Value: 0x25
+# CHECK-NEXT: Size: 1
+
+## From a.o: lds_big (align=16, size=20) -> offset 0x0
+# CHECK:      Name: lds_big
+# CHECK-NEXT: Value: 0x0
+# CHECK-NEXT: Size: 20
+
+## From b.o: lds_small (align=4, size=5) -> offset 0x20 (1 byte padding from 31)
+# CHECK:      Name: lds_small
+# CHECK-NEXT: Value: 0x20
+# CHECK-NEXT: Size: 5
+
+## From b.o: lds_med (align=8, size=7) -> offset 0x18 (4 bytes padding from 20)
+# CHECK:      Name: lds_med
+# CHECK-NEXT: Value: 0x18
+# CHECK-NEXT: Size: 7
+
+#--- a.s
+	.text
+	.globl f1
+	.p2align 8
+	.type f1, at function
+f1:
+	s_mov_b32 s0, lds_tiny at abs32@lo
+	s_mov_b32 s1, lds_big at abs32@lo
+	s_endpgm
+.Lf1_end:
+	.size f1, .Lf1_end-f1
+
+	.globl lds_tiny
+	.amdgpu_lds lds_tiny, 1, 1
+
+	.globl lds_big
+	.amdgpu_lds lds_big, 20, 16
+
+#--- b.s
+	.text
+	.globl f2
+	.p2align 8
+	.type f2, at function
+f2:
+	s_mov_b32 s0, lds_small at abs32@lo
+	s_mov_b32 s1, lds_med at abs32@lo
+	s_endpgm
+.Lf2_end:
+	.size f2, .Lf2_end-f2
+
+	.globl lds_small
+	.amdgpu_lds lds_small, 5, 4
+
+	.globl lds_med
+	.amdgpu_lds lds_med, 7, 8
diff --git a/lld/test/ELF/amdgpu-lds-link-time-per-kernel-sizes.s b/lld/test/ELF/amdgpu-lds-link-time-per-kernel-sizes.s
new file mode 100644
index 0000000000000..41ffc1628b40c
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-per-kernel-sizes.s
@@ -0,0 +1,240 @@
+# REQUIRES: amdgpu
+
+## Test that the linker computes LDS offsets and per-kernel LDS sizes for
+## disjoint kernel groups. This is the focused test for patching each kernel's
+## group_segment_fixed_size from the resolved LDS layout.
+##
+## TU1: kernel_a uses lds_a (256 bytes, align 16)
+## TU2: kernel_b uses lds_b (128 bytes, align 4)
+##
+## These are independent groups, so each allocates from offset 0:
+##   Group 1: lds_a at offset 0 -> kernel_a size = 0x100
+##   Group 2: lds_b at offset 0 -> kernel_b size = 0x080
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu1.s -o %t/tu1.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/tu2.s -o %t/tu2.o
+
+## Link.
+# RUN: ld.lld %t/tu1.o %t/tu2.o -o %t/out
+
+## Verify independent groups both allocate from offset 0.
+# RUN: llvm-readelf -s %t/out | FileCheck %s --check-prefix=SYM
+
+## Verify per-kernel LDS sizes via kernel descriptor patching and HSA metadata.
+# RUN: llvm-readobj --notes %t/out | FileCheck %s --check-prefix=META
+
+# SYM-DAG: 0000000000000000 {{.*}} lds_a
+# SYM-DAG: 0000000000000000 {{.*}} lds_b
+
+## kernel_a: lds_a at offset 0, size 256 -> group_segment_fixed_size = 256
+# META:      .group_segment_fixed_size: 256
+# META:      .name:           kernel_a
+
+## kernel_b: lds_b at offset 0, size 128 -> group_segment_fixed_size = 128
+# META:      .group_segment_fixed_size: 128
+# META:      .name:           kernel_b
+
+#--- tu1.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	kernel_a                        ; -- Begin function kernel_a
+	.p2align	8
+	.type	kernel_a, at function
+kernel_a:
+	s_endpgm
+.Lfunc_end0:
+	.size	kernel_a, .Lfunc_end0-kernel_a
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kernel_a.kd
+	.type	kernel_a.kd, at object
+	.size	kernel_a.kd, 64
+	.protected	kernel_a
+kernel_a.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	kernel_a at rel64-kernel_a.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Lkernel_a.num_vgpr, 2
+	.set .Lkernel_a.num_agpr, 0
+	.set .Lkernel_a.numbered_sgpr, 10
+	.set .Lkernel_a.num_named_barrier, 0
+	.set .Lkernel_a.private_seg_size, 0
+	.set .Lkernel_a.uses_vcc, 0
+	.set .Lkernel_a.uses_flat_scratch, 0
+	.set .Lkernel_a.has_dyn_sized_stack, 0
+	.set .Lkernel_a.has_recursion, 0
+	.set .Lkernel_a.has_indirect_call, 0
+	.amdgpu_info kernel_a
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use lds_a
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	lds_a
+	.amdgpu_lds lds_a, 256, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           kernel_a
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         kernel_a.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- tu2.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	kernel_b                        ; -- Begin function kernel_b
+	.p2align	8
+	.type	kernel_b, at function
+kernel_b:
+	s_endpgm
+.Lfunc_end0:
+	.size	kernel_b, .Lfunc_end0-kernel_b
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kernel_b.kd
+	.type	kernel_b.kd, at object
+	.size	kernel_b.kd, 64
+	.protected	kernel_b
+kernel_b.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	kernel_b at rel64-kernel_b.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11468864
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Lkernel_b.num_vgpr, 2
+	.set .Lkernel_b.num_agpr, 0
+	.set .Lkernel_b.numbered_sgpr, 10
+	.set .Lkernel_b.num_named_barrier, 0
+	.set .Lkernel_b.private_seg_size, 0
+	.set .Lkernel_b.uses_vcc, 0
+	.set .Lkernel_b.uses_flat_scratch, 0
+	.set .Lkernel_b.has_dyn_sized_stack, 0
+	.set .Lkernel_b.has_recursion, 0
+	.set .Lkernel_b.has_indirect_call, 0
+	.amdgpu_info kernel_b
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 10
+		.amdgpu_private_segment_size 0
+		.amdgpu_use lds_b
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	lds_b
+	.amdgpu_lds lds_b, 128, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           kernel_b
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         kernel_b.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-lds-link-time-random-layout.s b/lld/test/ELF/amdgpu-lds-link-time-random-layout.s
new file mode 100644
index 0000000000000..bfc9cdf8ac46b
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-random-layout.s
@@ -0,0 +1,432 @@
+# REQUIRES: amdgpu
+
+## Generated deterministic random LDS/kernel layouts for the per-kernel
+## frontier allocator. The generator was run ahead of time; lit does not
+## execute any script.
+
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %s -o %t.o
+# RUN: ld.lld %t.o -o %t
+# RUN: llvm-readelf -s %t | FileCheck %s --check-prefix=SYM
+
+## Case 0: 2 kernels, 4 LDS symbols, allocation order c0_lds1, c0_lds3, c0_lds2, c0_lds0
+##   c0_lds0: size=20, align=4, users={c0_k1}, offset=0x1c
+# SYM-DAG: 000000000000001c {{.*}} c0_lds0
+##   c0_lds1: size=24, align=4, users={c0_k0, c0_k1}, offset=0x0
+# SYM-DAG: 0000000000000000 {{.*}} c0_lds1
+##   c0_lds2: size=4, align=8, users={c0_k0}, offset=0x20
+# SYM-DAG: 0000000000000020 {{.*}} c0_lds2
+##   c0_lds3: size=4, align=4, users={c0_k0, c0_k1}, offset=0x18
+# SYM-DAG: 0000000000000018 {{.*}} c0_lds3
+## Case 1: 6 kernels, 6 LDS symbols, allocation order c1_lds2, c1_lds5, c1_lds1, c1_lds4, c1_lds3, c1_lds0
+##   c1_lds0: size=4, align=8, users={c1_k1, c1_k4}, offset=0x70
+# SYM-DAG: 0000000000000070 {{.*}} c1_lds0
+##   c1_lds1: size=32, align=4, users={c1_k0, c1_k2, c1_k3, c1_k4}, offset=0x38
+# SYM-DAG: 0000000000000038 {{.*}} c1_lds1
+##   c1_lds2: size=32, align=16, users={c1_k0, c1_k1, c1_k2, c1_k3, c1_k4, c1_k5}, offset=0x0
+# SYM-DAG: 0000000000000000 {{.*}} c1_lds2
+##   c1_lds3: size=8, align=4, users={c1_k2, c1_k4, c1_k5}, offset=0x68
+# SYM-DAG: 0000000000000068 {{.*}} c1_lds3
+##   c1_lds4: size=16, align=8, users={c1_k1, c1_k2, c1_k3}, offset=0x58
+# SYM-DAG: 0000000000000058 {{.*}} c1_lds4
+##   c1_lds5: size=24, align=4, users={c1_k0, c1_k1, c1_k2, c1_k3, c1_k4, c1_k5}, offset=0x20
+# SYM-DAG: 0000000000000020 {{.*}} c1_lds5
+## Case 2: 2 kernels, 4 LDS symbols, allocation order c2_lds1, c2_lds2, c2_lds0, c2_lds3
+##   c2_lds0: size=16, align=4, users={c2_k0, c2_k1}, offset=0x1c
+# SYM-DAG: 000000000000001c {{.*}} c2_lds0
+##   c2_lds1: size=12, align=8, users={c2_k0, c2_k1}, offset=0x0
+# SYM-DAG: 0000000000000000 {{.*}} c2_lds1
+##   c2_lds2: size=12, align=8, users={c2_k0, c2_k1}, offset=0x10
+# SYM-DAG: 0000000000000010 {{.*}} c2_lds2
+##   c2_lds3: size=4, align=4, users={c2_k0, c2_k1}, offset=0x2c
+# SYM-DAG: 000000000000002c {{.*}} c2_lds3
+## Case 3: 2 kernels, 5 LDS symbols, allocation order c3_lds1, c3_lds3, c3_lds2, c3_lds4, c3_lds0
+##   c3_lds0: size=12, align=4, users={c3_k0}, offset=0x90
+# SYM-DAG: 0000000000000090 {{.*}} c3_lds0
+##   c3_lds1: size=32, align=16, users={c3_k0, c3_k1}, offset=0x0
+# SYM-DAG: 0000000000000000 {{.*}} c3_lds1
+##   c3_lds2: size=32, align=4, users={c3_k0, c3_k1}, offset=0x50
+# SYM-DAG: 0000000000000050 {{.*}} c3_lds2
+##   c3_lds3: size=48, align=8, users={c3_k0, c3_k1}, offset=0x20
+# SYM-DAG: 0000000000000020 {{.*}} c3_lds3
+##   c3_lds4: size=32, align=4, users={c3_k0, c3_k1}, offset=0x70
+# SYM-DAG: 0000000000000070 {{.*}} c3_lds4
+## Case 4: 2 kernels, 3 LDS symbols, allocation order c4_lds2, c4_lds0, c4_lds1
+##   c4_lds0: size=48, align=16, users={c4_k0}, offset=0x20
+# SYM-DAG: 0000000000000020 {{.*}} c4_lds0
+##   c4_lds1: size=24, align=8, users={c4_k1}, offset=0x18
+# SYM-DAG: 0000000000000018 {{.*}} c4_lds1
+##   c4_lds2: size=20, align=8, users={c4_k0, c4_k1}, offset=0x0
+# SYM-DAG: 0000000000000000 {{.*}} c4_lds2
+## Case 5: 6 kernels, 6 LDS symbols, allocation order c5_lds5, c5_lds3, c5_lds4, c5_lds0, c5_lds1, c5_lds2
+##   c5_lds0: size=12, align=8, users={c5_k0, c5_k1, c5_k2, c5_k4}, offset=0x50
+# SYM-DAG: 0000000000000050 {{.*}} c5_lds0
+##   c5_lds1: size=8, align=8, users={c5_k0, c5_k3, c5_k4}, offset=0x60
+# SYM-DAG: 0000000000000060 {{.*}} c5_lds1
+##   c5_lds2: size=4, align=8, users={c5_k5}, offset=0x30
+# SYM-DAG: 0000000000000030 {{.*}} c5_lds2
+##   c5_lds3: size=32, align=16, users={c5_k0, c5_k3, c5_k4, c5_k5}, offset=0x10
+# SYM-DAG: 0000000000000010 {{.*}} c5_lds3
+##   c5_lds4: size=32, align=8, users={c5_k1, c5_k2, c5_k3, c5_k4}, offset=0x30
+# SYM-DAG: 0000000000000030 {{.*}} c5_lds4
+##   c5_lds5: size=12, align=4, users={c5_k0, c5_k1, c5_k2, c5_k3, c5_k4, c5_k5}, offset=0x0
+# SYM-DAG: 0000000000000000 {{.*}} c5_lds5
+
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+
+	.macro kernel name
+	.text
+	.globl	\name
+	.p2align	8
+	.type	\name, at function
+\name:
+	s_endpgm
+.L\name\()_end:
+	.size	\name, .L\name\()_end-\name
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	\name\().kd
+	.type	\name\().kd, at object
+	.size	\name\().kd, 64
+	.protected	\name
+\name\().kd:
+	.long	0
+	.long	0
+	.long	0
+	.long	0
+	.quad	\name at rel64-\name\().kd
+	.zero	20
+	.long	0
+	.long	0
+	.long	0
+	.short	0
+	.short	0
+	.long	0
+	.endm
+
+## Case 0
+	kernel c0_k0
+	kernel c0_k1
+
+	.amdgpu_info c0_k0
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c0_lds1
+		.amdgpu_use c0_lds2
+		.amdgpu_use c0_lds3
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c0_k1
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c0_lds0
+		.amdgpu_use c0_lds1
+		.amdgpu_use c0_lds3
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.globl	c0_lds0
+	.amdgpu_lds c0_lds0, 20, 4
+	.globl	c0_lds1
+	.amdgpu_lds c0_lds1, 24, 4
+	.globl	c0_lds2
+	.amdgpu_lds c0_lds2, 4, 8
+	.globl	c0_lds3
+	.amdgpu_lds c0_lds3, 4, 4
+
+## Case 1
+	kernel c1_k0
+	kernel c1_k1
+	kernel c1_k2
+	kernel c1_k3
+	kernel c1_k4
+	kernel c1_k5
+
+	.amdgpu_info c1_k0
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c1_lds1
+		.amdgpu_use c1_lds2
+		.amdgpu_use c1_lds5
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c1_k1
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c1_lds0
+		.amdgpu_use c1_lds2
+		.amdgpu_use c1_lds4
+		.amdgpu_use c1_lds5
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c1_k2
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c1_lds1
+		.amdgpu_use c1_lds2
+		.amdgpu_use c1_lds3
+		.amdgpu_use c1_lds4
+		.amdgpu_use c1_lds5
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c1_k3
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c1_lds1
+		.amdgpu_use c1_lds2
+		.amdgpu_use c1_lds4
+		.amdgpu_use c1_lds5
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c1_k4
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c1_lds0
+		.amdgpu_use c1_lds1
+		.amdgpu_use c1_lds2
+		.amdgpu_use c1_lds3
+		.amdgpu_use c1_lds5
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c1_k5
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c1_lds2
+		.amdgpu_use c1_lds3
+		.amdgpu_use c1_lds5
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.globl	c1_lds0
+	.amdgpu_lds c1_lds0, 4, 8
+	.globl	c1_lds1
+	.amdgpu_lds c1_lds1, 32, 4
+	.globl	c1_lds2
+	.amdgpu_lds c1_lds2, 32, 16
+	.globl	c1_lds3
+	.amdgpu_lds c1_lds3, 8, 4
+	.globl	c1_lds4
+	.amdgpu_lds c1_lds4, 16, 8
+	.globl	c1_lds5
+	.amdgpu_lds c1_lds5, 24, 4
+
+## Case 2
+	kernel c2_k0
+	kernel c2_k1
+
+	.amdgpu_info c2_k0
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c2_lds0
+		.amdgpu_use c2_lds1
+		.amdgpu_use c2_lds2
+		.amdgpu_use c2_lds3
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c2_k1
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c2_lds0
+		.amdgpu_use c2_lds1
+		.amdgpu_use c2_lds2
+		.amdgpu_use c2_lds3
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.globl	c2_lds0
+	.amdgpu_lds c2_lds0, 16, 4
+	.globl	c2_lds1
+	.amdgpu_lds c2_lds1, 12, 8
+	.globl	c2_lds2
+	.amdgpu_lds c2_lds2, 12, 8
+	.globl	c2_lds3
+	.amdgpu_lds c2_lds3, 4, 4
+
+## Case 3
+	kernel c3_k0
+	kernel c3_k1
+
+	.amdgpu_info c3_k0
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c3_lds0
+		.amdgpu_use c3_lds1
+		.amdgpu_use c3_lds2
+		.amdgpu_use c3_lds3
+		.amdgpu_use c3_lds4
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c3_k1
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c3_lds1
+		.amdgpu_use c3_lds2
+		.amdgpu_use c3_lds3
+		.amdgpu_use c3_lds4
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.globl	c3_lds0
+	.amdgpu_lds c3_lds0, 12, 4
+	.globl	c3_lds1
+	.amdgpu_lds c3_lds1, 32, 16
+	.globl	c3_lds2
+	.amdgpu_lds c3_lds2, 32, 4
+	.globl	c3_lds3
+	.amdgpu_lds c3_lds3, 48, 8
+	.globl	c3_lds4
+	.amdgpu_lds c3_lds4, 32, 4
+
+## Case 4
+	kernel c4_k0
+	kernel c4_k1
+
+	.amdgpu_info c4_k0
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c4_lds0
+		.amdgpu_use c4_lds2
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c4_k1
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c4_lds1
+		.amdgpu_use c4_lds2
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.globl	c4_lds0
+	.amdgpu_lds c4_lds0, 48, 16
+	.globl	c4_lds1
+	.amdgpu_lds c4_lds1, 24, 8
+	.globl	c4_lds2
+	.amdgpu_lds c4_lds2, 20, 8
+
+## Case 5
+	kernel c5_k0
+	kernel c5_k1
+	kernel c5_k2
+	kernel c5_k3
+	kernel c5_k4
+	kernel c5_k5
+
+	.amdgpu_info c5_k0
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c5_lds0
+		.amdgpu_use c5_lds1
+		.amdgpu_use c5_lds3
+		.amdgpu_use c5_lds5
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c5_k1
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c5_lds0
+		.amdgpu_use c5_lds4
+		.amdgpu_use c5_lds5
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c5_k2
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c5_lds0
+		.amdgpu_use c5_lds4
+		.amdgpu_use c5_lds5
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c5_k3
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c5_lds1
+		.amdgpu_use c5_lds3
+		.amdgpu_use c5_lds4
+		.amdgpu_use c5_lds5
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c5_k4
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c5_lds0
+		.amdgpu_use c5_lds1
+		.amdgpu_use c5_lds3
+		.amdgpu_use c5_lds4
+		.amdgpu_use c5_lds5
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info c5_k5
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use c5_lds2
+		.amdgpu_use c5_lds3
+		.amdgpu_use c5_lds5
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.globl	c5_lds0
+	.amdgpu_lds c5_lds0, 12, 8
+	.globl	c5_lds1
+	.amdgpu_lds c5_lds1, 8, 8
+	.globl	c5_lds2
+	.amdgpu_lds c5_lds2, 4, 8
+	.globl	c5_lds3
+	.amdgpu_lds c5_lds3, 32, 16
+	.globl	c5_lds4
+	.amdgpu_lds c5_lds4, 32, 8
+	.globl	c5_lds5
+	.amdgpu_lds c5_lds5, 12, 4
diff --git a/lld/test/ELF/amdgpu-lds-link-time-recursive-scc.s b/lld/test/ELF/amdgpu-lds-link-time-recursive-scc.s
new file mode 100644
index 0000000000000..487e79b7d074c
--- /dev/null
+++ b/lld/test/ELF/amdgpu-lds-link-time-recursive-scc.s
@@ -0,0 +1,161 @@
+# REQUIRES: amdgpu
+
+## Test LDS reachability through a recursive SCC. The kernel reaches lds_data
+## only through the H/A/M cycle, where A is the direct LDS user.
+
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %s -o %t.o
+# RUN: ld.lld %t.o -o %t
+# RUN: llvm-readobj --notes %t | FileCheck %s
+
+# CHECK: .group_segment_fixed_size: 128
+# CHECK: .name:           kernel
+# CHECK: .uses_dynamic_stack: true
+
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+
+	.text
+	.globl	kernel
+	.p2align	8
+	.type	kernel, at function
+kernel:
+	s_endpgm
+.Lkernel_end:
+	.size	kernel, .Lkernel_end-kernel
+
+	.globl	H
+	.p2align	6
+	.type	H, at function
+H:
+	s_setpc_b64 s[30:31]
+.LH_end:
+	.size	H, .LH_end-H
+
+	.globl	A
+	.p2align	6
+	.type	A, at function
+A:
+	s_setpc_b64 s[30:31]
+.LA_end:
+	.size	A, .LA_end-A
+
+	.globl	M
+	.p2align	6
+	.type	M, at function
+M:
+	s_setpc_b64 s[30:31]
+.LM_end:
+	.size	M, .LM_end-M
+
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kernel.kd
+	.type	kernel.kd, at object
+	.size	kernel.kd, 64
+	.protected	kernel
+kernel.kd:
+	.long	0
+	.long	0
+	.long	256
+	.long	0
+	.quad	kernel at rel64-kernel.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+
+	.text
+	.amdgpu_info kernel
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 10
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_call H
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info H
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 10
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_call A
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info A
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 10
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_use lds_data
+		.amdgpu_call M
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info M
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 10
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_call H
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 10
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 32
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	lds_data
+	.amdgpu_lds lds_data, 128, 16
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 256
+    .max_flat_workgroup_size: 1024
+    .name:           kernel
+    .private_segment_fixed_size: 0
+    .sgpr_count:     32
+    .sgpr_spill_count: 0
+    .symbol:         kernel.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     10
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-named-barrier-cross-tu.s b/lld/test/ELF/amdgpu-named-barrier-cross-tu.s
new file mode 100644
index 0000000000000..517916f8b303b
--- /dev/null
+++ b/lld/test/ELF/amdgpu-named-barrier-cross-tu.s
@@ -0,0 +1,193 @@
+# REQUIRES: amdgpu
+
+## Cross-TU named barrier collision test.
+##
+## Without link-time resolution, each TU would independently assign barrier
+## ID 1 to its local named barrier. When a kernel reaches both barriers
+## transitively, this causes a collision. The linker must assign distinct
+## barrier IDs to avoid this.
+##
+## TU A: kern uses bar_a (1 slot), calls helper
+## TU B: helper uses bar_b (1 slot)
+## After linking, kern reaches both bar_a and bar_b. The linker assigns
+## distinct IDs (e.g., bar_a=1, bar_b=2 or vice versa). NAMED_BAR_CNT=1
+## since ceil(2/4)=1.
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx1250 -filetype=obj %t/a.s -o %t/a.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx1250 -filetype=obj %t/b.s -o %t/b.o
+# RUN: ld.lld %t/a.o %t/b.o -o %t/out
+
+## Verify the resolved barrier symbols have distinct encoded addresses.
+## Each address is 0x802000 | (scope << 9) | (barId << 4).
+## bar_a and bar_b must have different barIds.
+# RUN: llvm-nm --format=posix %t/out | FileCheck %s --check-prefix=SYMS
+
+## Verify NAMED_BAR_CNT is correct in compute_pgm_rsrc3.
+## NAMED_BAR_CNT=1 is encoded as 0x00004000, printed below as little-endian
+## bytes 00400000.
+# RUN: llvm-objdump -s -j .rodata %t/out | FileCheck %s --check-prefix=KD
+
+## The two barrier symbols must have different addresses (distinct barrier IDs).
+## Both should match the barrier encoding pattern: 0x802010 (ID=1) or 0x802020 (ID=2).
+# SYMS-DAG: __amdgpu_named_barrier.bar_a{{[^ ]*}} A {{[0-9a-f]+}}
+# SYMS-DAG: __amdgpu_named_barrier.bar_b{{[^ ]*}} A {{[0-9a-f]+}}
+
+# KD: {{[0-9a-f]+}} 00000000 00000000 00000000 00400000
+
+#--- a.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx1250"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	kern                            ; -- Begin function kern
+	.p2align	8
+	.type	kern, at function
+kern:
+	s_endpgm
+.Lfunc_end0:
+	.size	kern, .Lfunc_end0-kern
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kern.kd
+	.type	kern.kd, at object
+	.size	kern.kd, 64
+	.protected	kern
+kern.kd:
+	.long	0
+	.long	0
+	.long	256
+	.long	0
+	.quad	kern at rel64-kern.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	3222208513
+	.long	5008
+	.short	1054
+	.short	0
+	.long	0
+	.text
+	.set .Lkern.num_vgpr, 32
+	.set .Lkern.num_agpr, 0
+	.set .Lkern.numbered_sgpr, 33
+	.set .Lkern.num_named_barrier, 0
+	.set .Lkern.private_seg_size, 0
+	.set .Lkern.uses_vcc, 1
+	.set .Lkern.uses_flat_scratch, 0
+	.set .Lkern.has_dyn_sized_stack, 0
+	.set .Lkern.has_recursion, 0
+	.set .Lkern.has_indirect_call, 0
+	.text
+	.amdgpu_info kern
+		.amdgpu_flags 1
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_use __amdgpu_named_barrier.bar_a.a8fb08c539c29f35a63da55cd03bd885
+		.amdgpu_call helper
+		.amdgpu_occupancy 8
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.text
+	.globl	__amdgpu_named_barrier.bar_a.a8fb08c539c29f35a63da55cd03bd885
+	.amdgpu_lds __amdgpu_named_barrier.bar_a.a8fb08c539c29f35a63da55cd03bd885, 16, 4
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 256
+    .max_flat_workgroup_size: 1024
+    .name:           kern
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         kern.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 32
+amdhsa.target:   amdgcn-amd-amdhsa--gfx1250
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- b.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx1250"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	helper                          ; -- Begin function helper
+	.p2align	7
+	.type	helper, at function
+helper:
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	helper, .Lfunc_end0-helper
+	.set .Lhelper.num_vgpr, 0
+	.set .Lhelper.num_agpr, 0
+	.set .Lhelper.numbered_sgpr, 32
+	.set .Lhelper.num_named_barrier, 0
+	.set .Lhelper.private_seg_size, 0
+	.set .Lhelper.uses_vcc, 0
+	.set .Lhelper.uses_flat_scratch, 0
+	.set .Lhelper.has_dyn_sized_stack, 0
+	.set .Lhelper.has_recursion, 0
+	.set .Lhelper.has_indirect_call, 0
+	.text
+	.amdgpu_info helper
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 0
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_use __amdgpu_named_barrier.bar_b.957946e05970693deaaae396388e1c50
+		.amdgpu_occupancy 8
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 32
+	.set amdgpu.max_num_named_barrier, 0
+	.text
+	.globl	__amdgpu_named_barrier.bar_b.957946e05970693deaaae396388e1c50
+	.amdgpu_lds __amdgpu_named_barrier.bar_b.957946e05970693deaaae396388e1c50, 16, 4
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:  []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx1250
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-named-barrier-grouped.s b/lld/test/ELF/amdgpu-named-barrier-grouped.s
new file mode 100644
index 0000000000000..bc2d88b712ad9
--- /dev/null
+++ b/lld/test/ELF/amdgpu-named-barrier-grouped.s
@@ -0,0 +1,109 @@
+# REQUIRES: amdgpu
+
+## Test per-kernel named-barrier frontiers. A and B are allocated first because
+## they have the most users. C can still reuse B's IDs because no kernel reaches
+## both B and C, even though A and B connect all three kernels into one old-style
+## barrier group.
+##
+##   K1: A, C
+##   K2: A, B
+##   K3: B, D
+##
+## With connected-component allocation, C would be assigned after A and B, so K1
+## would require 6 named barriers. The frontier allocator places C at ID 3, so
+## K1 only requires 4 named barriers and NAMED_BAR_CNT stays 1.
+
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx1250 -filetype=obj %s -o %t.o
+# RUN: ld.lld %t.o -o %t
+# RUN: llvm-nm --format=posix %t | FileCheck %s --check-prefix=SYM
+# RUN: llvm-objdump -s -j .rodata %t | FileCheck %s --check-prefix=KD
+
+# SYM-DAG: __amdgpu_named_barrier.a A {{0*}}802010
+# SYM-DAG: __amdgpu_named_barrier.b A {{0*}}802030
+# SYM-DAG: __amdgpu_named_barrier.c A {{0*}}802030
+# SYM-DAG: __amdgpu_named_barrier.d A {{0*}}802050
+
+## K1 and K2 use IDs 1..4, so NAMED_BAR_CNT=1. K3 uses IDs 3..6, so
+## NAMED_BAR_CNT=2.
+# KD: {{[0-9a-f]+}} 00000000 00000000 00000000 00400000
+# KD: {{[0-9a-f]+}} 00000000 00000000 00000000 00400000
+# KD: {{[0-9a-f]+}} 00000000 00000000 00000000 00800000
+
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx1250"
+	.amdhsa_code_object_version 6
+
+	.macro kernel name
+	.text
+	.globl	\name
+	.p2align	8
+	.type	\name, at function
+\name:
+	s_endpgm
+.L\name\()_end:
+	.size	\name, .L\name\()_end-\name
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	\name\().kd
+	.type	\name\().kd, at object
+	.size	\name\().kd, 64
+	.protected	\name
+\name\().kd:
+	.long	0
+	.long	0
+	.long	0
+	.long	0
+	.quad	\name at rel64-\name\().kd
+	.zero	20
+	.long	0
+	.long	0
+	.long	0
+	.short	0
+	.short	0
+	.long	0
+	.endm
+
+	kernel K1
+	kernel K2
+	kernel K3
+
+	.amdgpu_info K1
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use __amdgpu_named_barrier.a
+		.amdgpu_use __amdgpu_named_barrier.c
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K2
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use __amdgpu_named_barrier.a
+		.amdgpu_use __amdgpu_named_barrier.b
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info K3
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 1
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 1
+		.amdgpu_private_segment_size 0
+		.amdgpu_use __amdgpu_named_barrier.b
+		.amdgpu_use __amdgpu_named_barrier.d
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.globl	__amdgpu_named_barrier.a
+	.amdgpu_lds __amdgpu_named_barrier.a, 32, 4
+	.globl	__amdgpu_named_barrier.b
+	.amdgpu_lds __amdgpu_named_barrier.b, 32, 4
+	.globl	__amdgpu_named_barrier.c
+	.amdgpu_lds __amdgpu_named_barrier.c, 32, 4
+	.globl	__amdgpu_named_barrier.d
+	.amdgpu_lds __amdgpu_named_barrier.d, 32, 4
diff --git a/lld/test/ELF/amdgpu-object-linking-asm-roundtrip.s b/lld/test/ELF/amdgpu-object-linking-asm-roundtrip.s
new file mode 100644
index 0000000000000..62a7b4b91d446
--- /dev/null
+++ b/lld/test/ELF/amdgpu-object-linking-asm-roundtrip.s
@@ -0,0 +1,145 @@
+# REQUIRES: amdgpu
+
+## Test that the assembly form of AMDGPU object-linking metadata can be
+## assembled by llvm-mc and consumed by ld.lld. This covers the save-temps
+## style path without depending on compiler code generation in an lld test.
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/a.s -o %t/a.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/b.s -o %t/b.o
+# RUN: ld.lld %t/a.o %t/b.o -o %t/out
+# RUN: llvm-readobj --notes %t/out | FileCheck %s
+
+# CHECK:      .name: kernel
+# CHECK:      .sgpr_count:
+# CHECK:      .vgpr_count:
+
+#--- a.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	kernel
+	.p2align	8
+	.type	kernel, at function
+kernel:
+	s_endpgm
+.Lkernel_end:
+	.size	kernel, .Lkernel_end-kernel
+
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kernel.kd
+	.type	kernel.kd, at object
+	.size	kernel.kd, 64
+	.protected	kernel
+kernel.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	kernel at rel64-kernel.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+
+	.text
+	.amdgpu_info kernel
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_call helper
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           kernel
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .sgpr_spill_count: 0
+    .symbol:         kernel.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- b.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	helper
+	.p2align	6
+	.type	helper, at function
+helper:
+	s_setpc_b64 s[30:31]
+.Lhelper_end:
+	.size	helper, .Lhelper_end-helper
+
+	.amdgpu_info helper
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 8
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 8
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 32
+	.set amdgpu.max_num_named_barrier, 0
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:  []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-object-linking-malformed-strtab.s b/lld/test/ELF/amdgpu-object-linking-malformed-strtab.s
new file mode 100644
index 0000000000000..6919609052a77
--- /dev/null
+++ b/lld/test/ELF/amdgpu-object-linking-malformed-strtab.s
@@ -0,0 +1,209 @@
+# REQUIRES: amdgpu
+
+## The type-id string in .amdgpu.strtab is intentionally not NUL-terminated.
+## The linker should bound the read to the section contents and still match the
+## indirect call to the address-taken function.
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/a.s -o %t/a.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/b.s -o %t/b.o
+# RUN: ld.lld %t/a.o %t/b.o -o %t/out
+# RUN: llvm-readobj --notes %t/out | FileCheck %s
+
+# CHECK:      .group_segment_fixed_size: 128
+# CHECK:      .name: kernel
+
+#--- a.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	target_func
+	.p2align	6
+	.type	target_func, at function
+target_func:
+	s_setpc_b64 s[30:31]
+.Ltarget_func_end:
+	.size	target_func, .Ltarget_func_end-target_func
+
+	.globl	lds_var
+	.amdgpu_lds lds_var, 128, 16
+
+	.section	.amdgpu.info,"e", at progbits
+	.byte	1
+	.byte	8
+	.quad	target_func
+	.byte	2
+	.byte	4
+	.long	0
+	.byte	4
+	.byte	4
+	.long	4
+	.byte	3
+	.byte	4
+	.long	4
+	.byte	6
+	.byte	4
+	.long	0
+	.byte	11
+	.byte	4
+	.long	8
+	.byte	12
+	.byte	4
+	.long	64
+	.byte	7
+	.byte	8
+	.quad	lds_var
+	.byte	10
+	.byte	4
+	.long	0
+
+	.section	.amdgpu.strtab,"e", at progbits
+	.ascii	"vi"
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels: []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- b.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	caller
+	.p2align	6
+	.type	caller, at function
+caller:
+	s_setpc_b64 s[30:31]
+.Lcaller_end:
+	.size	caller, .Lcaller_end-caller
+
+	.globl	kernel
+	.p2align	8
+	.type	kernel, at function
+kernel:
+	s_endpgm
+.Lkernel_end:
+	.size	kernel, .Lkernel_end-kernel
+
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kernel.kd
+	.type	kernel.kd, at object
+	.size	kernel.kd, 64
+	.protected	kernel
+kernel.kd:
+	.long	0
+	.long	0
+	.long	0
+	.long	0
+	.long	0
+	.quad	kernel at rel64-kernel.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	0
+	.long	0
+	.short	0
+	.short	0
+	.long	0
+
+	.section	.amdgpu.info,"e", at progbits
+	.byte	1
+	.byte	8
+	.quad	caller
+	.byte	2
+	.byte	4
+	.long	0
+	.byte	4
+	.byte	4
+	.long	4
+	.byte	3
+	.byte	4
+	.long	4
+	.byte	6
+	.byte	4
+	.long	0
+	.byte	11
+	.byte	4
+	.long	8
+	.byte	12
+	.byte	4
+	.long	64
+	.byte	9
+	.byte	4
+	.long	0
+	.byte	1
+	.byte	8
+	.quad	kernel
+	.byte	2
+	.byte	4
+	.long	0
+	.byte	4
+	.byte	4
+	.long	4
+	.byte	3
+	.byte	4
+	.long	4
+	.byte	6
+	.byte	4
+	.long	0
+	.byte	11
+	.byte	4
+	.long	8
+	.byte	12
+	.byte	4
+	.long	64
+	.byte	8
+	.byte	8
+	.quad	caller
+
+	.section	.amdgpu.strtab,"e", at progbits
+	.ascii	"vi"
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 0
+    .max_flat_workgroup_size: 1024
+    .name:           kernel
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .symbol:         kernel.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-object-linking-target-id.s b/lld/test/ELF/amdgpu-object-linking-target-id.s
new file mode 100644
index 0000000000000..ecdd24ee1917a
--- /dev/null
+++ b/lld/test/ELF/amdgpu-object-linking-target-id.s
@@ -0,0 +1,226 @@
+# REQUIRES: amdgpu
+
+## Test that the linker rejects objects with incompatible target IDs when both
+## participate in AMDGPU object linking (i.e. have .amdgpu.info sections).
+
+# RUN: split-file %s %t
+
+## --- Incompatible GPU architecture ---
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/func-gfx900.s -o %t/func-gfx900.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx1030 -filetype=obj %t/func-gfx1030.s -o %t/func-gfx1030.o
+# RUN: not ld.lld %t/func-gfx900.o %t/func-gfx1030.o -o /dev/null 2>&1 | FileCheck %s --check-prefix=ARCH
+
+# ARCH: error: AMDGPU object linking: incompatible GPU architecture
+
+## --- Incompatible xnack ---
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -mattr=+xnack -filetype=obj %t/func-xnack-on.s -o %t/func-xnack-on.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -mattr=-xnack -filetype=obj %t/func-xnack-off.s -o %t/func-xnack-off.o
+# RUN: not ld.lld %t/func-xnack-on.o %t/func-xnack-off.o -o /dev/null 2>&1 | FileCheck %s --check-prefix=XNACK
+
+# XNACK: error: AMDGPU object linking: incompatible xnack setting
+
+## --- Incompatible sramecc ---
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx908 -mattr=+sramecc -filetype=obj %t/func-sramecc-on.s -o %t/func-sramecc-on.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx908 -mattr=-sramecc -filetype=obj %t/func-sramecc-off.s -o %t/func-sramecc-off.o
+# RUN: not ld.lld %t/func-sramecc-on.o %t/func-sramecc-off.o -o /dev/null 2>&1 | FileCheck %s --check-prefix=SRAMECC
+# RUN: ld.lld %t/func-sramecc-on.o -o %t/sramecc-on
+
+# SRAMECC: error: AMDGPU object linking: incompatible sramecc setting
+
+#--- func-gfx900.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	func_a
+	.p2align	2
+	.type	func_a, at function
+func_a:
+	s_setpc_b64 s[30:31]
+.Lfunc_end_a:
+	.size	func_a, .Lfunc_end_a-func_a
+
+	.amdgpu_info func_a
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 4
+		.amdgpu_num_sgpr 4
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 8
+		.amdgpu_wave_size 64
+	.end_amdgpu_info
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels: []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- func-gfx1030.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx1030"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	func_b
+	.p2align	2
+	.type	func_b, at function
+func_b:
+	s_setpc_b64 s[30:31]
+.Lfunc_end_b:
+	.size	func_b, .Lfunc_end_b-func_b
+
+	.amdgpu_info func_b
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 4
+		.amdgpu_num_sgpr 4
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 8
+		.amdgpu_wave_size 32
+	.end_amdgpu_info
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels: []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx1030
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- func-xnack-on.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900:xnack+"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	func_c
+	.p2align	2
+	.type	func_c, at function
+func_c:
+	s_setpc_b64 s[30:31]
+.Lfunc_end_c:
+	.size	func_c, .Lfunc_end_c-func_c
+
+	.amdgpu_info func_c
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 4
+		.amdgpu_num_sgpr 4
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 8
+		.amdgpu_wave_size 64
+	.end_amdgpu_info
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels: []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900:xnack+
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- func-xnack-off.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900:xnack-"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	func_d
+	.p2align	2
+	.type	func_d, at function
+func_d:
+	s_setpc_b64 s[30:31]
+.Lfunc_end_d:
+	.size	func_d, .Lfunc_end_d-func_d
+
+	.amdgpu_info func_d
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 4
+		.amdgpu_num_sgpr 4
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 8
+		.amdgpu_wave_size 64
+	.end_amdgpu_info
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels: []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900:xnack-
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- func-sramecc-on.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx908:sramecc+"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	func_e
+	.p2align	2
+	.type	func_e, at function
+func_e:
+	s_setpc_b64 s[30:31]
+.Lfunc_end_e:
+	.size	func_e, .Lfunc_end_e-func_e
+
+	.amdgpu_info func_e
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 4
+		.amdgpu_num_sgpr 4
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 8
+		.amdgpu_wave_size 64
+	.end_amdgpu_info
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels: []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx908:sramecc+
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- func-sramecc-off.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx908:sramecc-"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	func_f
+	.p2align	2
+	.type	func_f, at function
+func_f:
+	s_setpc_b64 s[30:31]
+.Lfunc_end_f:
+	.size	func_f, .Lfunc_end_f-func_f
+
+	.amdgpu_info func_f
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 4
+		.amdgpu_num_sgpr 4
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 8
+		.amdgpu_wave_size 64
+	.end_amdgpu_info
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels: []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx908:sramecc-
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-object-linking-wave-size.s b/lld/test/ELF/amdgpu-object-linking-wave-size.s
new file mode 100644
index 0000000000000..e82154b68bc1c
--- /dev/null
+++ b/lld/test/ELF/amdgpu-object-linking-wave-size.s
@@ -0,0 +1,129 @@
+# REQUIRES: amdgpu
+
+## Test that the linker rejects cross-TU calls between functions compiled with
+## different wavefront sizes.
+
+# RUN: split-file %s %t
+
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx1030 -filetype=obj %t/a.s -o %t/a.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx1030 -filetype=obj %t/b.s -o %t/b.o
+# RUN: not ld.lld %t/a.o %t/b.o -o /dev/null 2>&1 | FileCheck %s
+
+# CHECK: error: AMDGPU object linking: wave size mismatch in call from 'kernel' (wave32) to 'helper' (wave64)
+
+#--- a.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx1030"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	kernel
+	.p2align	8
+	.type	kernel, at function
+kernel:
+	s_endpgm
+.Lfunc_end0:
+	.size	kernel, .Lfunc_end0-kernel
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kernel.kd
+	.type	kernel.kd, at object
+	.size	kernel.kd, 64
+	.protected	kernel
+kernel.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.long	0
+	.quad	kernel at rel64-kernel.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	1063
+	.short	0
+	.long	0
+	.text
+	.amdgpu_info kernel
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 4
+		.amdgpu_wave_size 32
+		.amdgpu_call helper
+	.end_amdgpu_info
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           kernel
+    .private_segment_fixed_size: 0
+    .sgpr_count:     39
+    .symbol:         kernel.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     32
+    .wavefront_size: 32
+amdhsa.target:   amdgcn-amd-amdhsa--gfx1030
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- b.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx1030"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	helper
+	.p2align	2
+	.type	helper, at function
+helper:
+	s_setpc_b64 s[30:31]
+.Lfunc_end1:
+	.size	helper, .Lfunc_end1-helper
+
+	.amdgpu_info helper
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 10
+		.amdgpu_num_sgpr 8
+		.amdgpu_private_segment_size 16
+		.amdgpu_occupancy 8
+		.amdgpu_wave_size 64
+	.end_amdgpu_info
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels: []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx1030
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-resource-usage-alias.s b/lld/test/ELF/amdgpu-resource-usage-alias.s
new file mode 100644
index 0000000000000..34d452c191e94
--- /dev/null
+++ b/lld/test/ELF/amdgpu-resource-usage-alias.s
@@ -0,0 +1,173 @@
+# REQUIRES: amdgpu
+
+## Test that the linker resolves function aliases during resource propagation.
+## In the C++ Itanium ABI, C1 (complete object) constructors are often aliases
+## to C2 (base object) constructors. The compiler emits a resource entry
+## only for C2; C1 appears as a stub without has_local_res.
+## Without alias resolution, the linker would report "incomplete resource usage".
+##
+## Call graph: kernel -> _ZN3FooC1Ev (alias) -> _ZN3FooC2Ev (real definition)
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/a.s -o %t/a.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/b.s -o %t/b.o
+# RUN: ld.lld %t/a.o %t/b.o -o %t/out
+
+## Verify the linker succeeds and patches HSA metadata.
+# RUN: llvm-readobj --notes %t/out | FileCheck %s
+
+# CHECK:      .name: test_kernel
+# CHECK:      .sgpr_count:
+# CHECK:      .vgpr_count:
+
+#--- a.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	test_kernel                     ; -- Begin function test_kernel
+	.p2align	8
+	.type	test_kernel, at function
+test_kernel:
+	s_endpgm
+.Lfunc_end0:
+	.size	test_kernel, .Lfunc_end0-test_kernel
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	test_kernel.kd
+	.type	test_kernel.kd, at object
+	.size	test_kernel.kd, 64
+	.protected	test_kernel
+test_kernel.kd:
+	.long	0
+	.long	0
+	.long	256
+	.long	0
+	.quad	test_kernel at rel64-test_kernel.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Ltest_kernel.num_vgpr, 32
+	.set .Ltest_kernel.num_agpr, 0
+	.set .Ltest_kernel.numbered_sgpr, 33
+	.set .Ltest_kernel.num_named_barrier, 0
+	.set .Ltest_kernel.private_seg_size, 0
+	.set .Ltest_kernel.uses_vcc, 1
+	.set .Ltest_kernel.uses_flat_scratch, 1
+	.set .Ltest_kernel.has_dyn_sized_stack, 0
+	.set .Ltest_kernel.has_recursion, 0
+	.set .Ltest_kernel.has_indirect_call, 0
+	.amdgpu_info test_kernel
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_call _ZN3FooC1Ev
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 256
+    .max_flat_workgroup_size: 1024
+    .name:           test_kernel
+    .private_segment_fixed_size: 0
+    .sgpr_count:     39
+    .sgpr_spill_count: 0
+    .symbol:         test_kernel.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     32
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- b.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	_ZN3FooC2Ev                     ; -- Begin function _ZN3FooC2Ev
+	.p2align	6
+	.type	_ZN3FooC2Ev, at function
+_ZN3FooC2Ev:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	_ZN3FooC2Ev, .Lfunc_end0-_ZN3FooC2Ev
+	.set .L_ZN3FooC2Ev.num_vgpr, 8
+	.set .L_ZN3FooC2Ev.num_agpr, 0
+	.set .L_ZN3FooC2Ev.numbered_sgpr, 32
+	.set .L_ZN3FooC2Ev.num_named_barrier, 0
+	.set .L_ZN3FooC2Ev.private_seg_size, 0
+	.set .L_ZN3FooC2Ev.uses_vcc, 0
+	.set .L_ZN3FooC2Ev.uses_flat_scratch, 0
+	.set .L_ZN3FooC2Ev.has_dyn_sized_stack, 0
+	.set .L_ZN3FooC2Ev.has_recursion, 0
+	.set .L_ZN3FooC2Ev.has_indirect_call, 0
+	.amdgpu_info _ZN3FooC2Ev
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 8
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_typeid "v"
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 8
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 32
+	.set amdgpu.max_num_named_barrier, 0
+	.globl	_ZN3FooC1Ev
+	.type	_ZN3FooC1Ev, at function
+.set _ZN3FooC1Ev, _ZN3FooC2Ev
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:  []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-resource-usage-recursive-scc.s b/lld/test/ELF/amdgpu-resource-usage-recursive-scc.s
new file mode 100644
index 0000000000000..48a616c317b69
--- /dev/null
+++ b/lld/test/ELF/amdgpu-resource-usage-recursive-scc.s
@@ -0,0 +1,257 @@
+# REQUIRES: amdgpu
+
+## Resource usage propagation through recursive SCCs must reach a fixed point.
+## In this graph, H/A/M form a cycle and A calls a high-resource leaf C:
+##
+##   kernel_h -> H -> A -> M -> H
+##                    `-> C
+##   kernel_m -> M
+##
+## Both kernels must receive C's resource usage through the SCC, regardless of
+## which SCC member they enter through.
+
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %s -o %t.o
+# RUN: ld.lld %t.o -o %t
+# RUN: llvm-readobj --notes %t | FileCheck %s
+
+# CHECK:      .name: kernel_h
+# CHECK:      .private_segment_fixed_size: 80
+# CHECK:      .uses_dynamic_stack: true
+# CHECK:      .vgpr_count: 100
+# CHECK:      .name: kernel_m
+# CHECK:      .private_segment_fixed_size: 80
+# CHECK:      .uses_dynamic_stack: true
+# CHECK:      .vgpr_count: 100
+
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+
+	.text
+	.globl	kernel_h
+	.p2align	8
+	.type	kernel_h, at function
+kernel_h:
+	s_endpgm
+.Lkernel_h_end:
+	.size	kernel_h, .Lkernel_h_end-kernel_h
+
+	.globl	kernel_m
+	.p2align	8
+	.type	kernel_m, at function
+kernel_m:
+	s_endpgm
+.Lkernel_m_end:
+	.size	kernel_m, .Lkernel_m_end-kernel_m
+
+	.globl	H
+	.p2align	6
+	.type	H, at function
+H:
+	s_setpc_b64 s[30:31]
+.LH_end:
+	.size	H, .LH_end-H
+
+	.globl	A
+	.p2align	6
+	.type	A, at function
+A:
+	s_setpc_b64 s[30:31]
+.LA_end:
+	.size	A, .LA_end-A
+
+	.globl	M
+	.p2align	6
+	.type	M, at function
+M:
+	s_setpc_b64 s[30:31]
+.LM_end:
+	.size	M, .LM_end-M
+
+	.globl	C
+	.p2align	6
+	.type	C, at function
+C:
+	s_setpc_b64 s[30:31]
+.LC_end:
+	.size	C, .LC_end-C
+
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kernel_h.kd
+	.type	kernel_h.kd, at object
+	.size	kernel_h.kd, 64
+	.protected	kernel_h
+kernel_h.kd:
+	.long	0
+	.long	0
+	.long	256
+	.long	0
+	.quad	kernel_h at rel64-kernel_h.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+
+	.p2align	6, 0x0
+	.globl	kernel_m.kd
+	.type	kernel_m.kd, at object
+	.size	kernel_m.kd, 64
+	.protected	kernel_m
+kernel_m.kd:
+	.long	0
+	.long	0
+	.long	256
+	.long	0
+	.quad	kernel_m at rel64-kernel_m.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+
+	.text
+	.amdgpu_info kernel_h
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 10
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_call H
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info kernel_m
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 10
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_call M
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info H
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 10
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 16
+		.amdgpu_call A
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info A
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 10
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 16
+		.amdgpu_call M
+		.amdgpu_call C
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info M
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 10
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 16
+		.amdgpu_call H
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info C
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 100
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 64
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 100
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 32
+	.set amdgpu.max_num_named_barrier, 0
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 256
+    .max_flat_workgroup_size: 1024
+    .name:           kernel_h
+    .private_segment_fixed_size: 0
+    .sgpr_count:     32
+    .sgpr_spill_count: 0
+    .symbol:         kernel_h.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     10
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 256
+    .max_flat_workgroup_size: 1024
+    .name:           kernel_m
+    .private_segment_fixed_size: 0
+    .sgpr_count:     32
+    .sgpr_spill_count: 0
+    .symbol:         kernel_m.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     10
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-resource-usage-section-sym.s b/lld/test/ELF/amdgpu-resource-usage-section-sym.s
new file mode 100644
index 0000000000000..4198258f27597
--- /dev/null
+++ b/lld/test/ELF/amdgpu-resource-usage-section-sym.s
@@ -0,0 +1,236 @@
+# REQUIRES: amdgpu
+
+## Test that link-time resource propagation handles functions referenced via
+## STT_SECTION symbols in relocations against the `.amdgpu.info` section.
+##
+## When a local (internal-linkage) function is placed in its own text section
+## (e.g. .text.unlikely. due to the cold attribute), ELF assemblers
+## canonically convert relocations targeting that function from the named
+## local symbol to section_symbol + addend.  The linker must resolve these
+## section symbol references back to the named function symbol so that it
+## can build the call graph and propagate resource usage from `.amdgpu.info`
+## correctly.
+##
+## Without this handling, the callee's resource info (scratch size, VGPR
+## count, etc.) would be silently dropped and the kernel descriptor would
+## be patched with incorrect values.
+
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %s -o %t.o
+# RUN: llvm-readobj -S %t.o | FileCheck %s --check-prefix=SHDR
+# RUN: ld.lld %t.o -o %t.out
+# RUN: llvm-readobj --notes %t.out | FileCheck %s
+
+# SHDR: Name: .amdgpu.info
+
+## The kernel itself has private_segment_fixed_size 0 but calls callee which
+## has 132 bytes of scratch.  After propagation, the kernel should reflect the
+## callee's scratch requirement.
+# CHECK:       .name:           kernel
+# CHECK:       .private_segment_fixed_size: 132
+
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+
+## callee lives in .text.unlikely. — a separate text section.
+## Because callee is local (not .globl), llvm-mc will emit its relocations
+## in `.amdgpu.info` using the STT_SECTION symbol for
+## .text.unlikely. rather than the named symbol "callee".
+	.section	.text.unlikely.,"ax", at progbits
+	.p2align	6
+	.type	callee, at function
+callee:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_lshr_b32 s4, s32, 6
+	v_lshl_add_u32 v1, v0, 2, s4
+	buffer_store_dword v0, v1, s[0:3], 0 offen
+	s_waitcnt vmcnt(0)
+	buffer_load_dword v0, v1, s[0:3], 0 offen glc
+	s_waitcnt vmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	callee, .Lfunc_end0-callee
+
+	.text
+	.globl	kernel
+	.p2align	8
+	.type	kernel, at function
+kernel:
+	s_add_u32 flat_scratch_lo, s12, s17
+	s_addc_u32 flat_scratch_hi, s13, 0
+	s_mov_b32 s13, s15
+	s_load_dwordx2 s[18:19], s[8:9], 0x0
+	s_load_dword s15, s[8:9], 0x8
+	s_add_u32 s0, s0, s17
+	s_addc_u32 s1, s1, 0
+	s_add_u32 s8, s8, 16
+	s_addc_u32 s9, s9, 0
+	v_lshlrev_b32_e32 v2, 20, v2
+	v_lshlrev_b32_e32 v1, 10, v1
+	s_mov_b32 s12, s14
+	s_getpc_b64 s[20:21]
+	s_add_u32 s20, s20, callee at rel32@lo+4
+	s_addc_u32 s21, s21, callee at rel32@hi+12
+	v_or3_b32 v31, v0, v1, v2
+	s_mov_b32 s14, s16
+	s_waitcnt lgkmcnt(0)
+	v_mov_b32_e32 v0, s15
+	s_mov_b32 s32, 0
+	v_mov_b32_e32 v3, 0
+	s_swappc_b64 s[30:31], s[20:21]
+	global_store_dword v3, v0, s[18:19]
+	s_endpgm
+
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kernel.kd
+	.type	kernel.kd, at object
+	.size	kernel.kd, 64
+	.protected	kernel
+kernel.kd:
+	.long	0
+	.long	0
+	.long	272
+	.long	0
+	.quad	kernel at rel64-kernel.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+
+	.text
+.Lfunc_end1:
+	.size	kernel, .Lfunc_end1-kernel
+
+## Object linking metadata: callee has 132 bytes of scratch, kernel has 0.
+## Flags: 0x1=is_kernel, 0x4=uses_flat_scratch (kernel only).
+	.amdgpu_info callee
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 2
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 132
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_info kernel
+		.amdgpu_flags 2
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_call callee
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .args:
+      - .address_space:  global
+        .name:           out
+        .offset:         0
+        .size:           8
+        .value_kind:     global_buffer
+      - .name:           x
+        .offset:         8
+        .size:           4
+        .value_kind:     by_value
+      - .offset:         16
+        .size:           4
+        .value_kind:     hidden_block_count_x
+      - .offset:         20
+        .size:           4
+        .value_kind:     hidden_block_count_y
+      - .offset:         24
+        .size:           4
+        .value_kind:     hidden_block_count_z
+      - .offset:         28
+        .size:           2
+        .value_kind:     hidden_group_size_x
+      - .offset:         30
+        .size:           2
+        .value_kind:     hidden_group_size_y
+      - .offset:         32
+        .size:           2
+        .value_kind:     hidden_group_size_z
+      - .offset:         34
+        .size:           2
+        .value_kind:     hidden_remainder_x
+      - .offset:         36
+        .size:           2
+        .value_kind:     hidden_remainder_y
+      - .offset:         38
+        .size:           2
+        .value_kind:     hidden_remainder_z
+      - .offset:         56
+        .size:           8
+        .value_kind:     hidden_global_offset_x
+      - .offset:         64
+        .size:           8
+        .value_kind:     hidden_global_offset_y
+      - .offset:         72
+        .size:           8
+        .value_kind:     hidden_global_offset_z
+      - .offset:         80
+        .size:           2
+        .value_kind:     hidden_grid_dims
+      - .offset:         96
+        .size:           8
+        .value_kind:     hidden_hostcall_buffer
+      - .offset:         104
+        .size:           8
+        .value_kind:     hidden_multigrid_sync_arg
+      - .offset:         112
+        .size:           8
+        .value_kind:     hidden_heap_v1
+      - .offset:         120
+        .size:           8
+        .value_kind:     hidden_default_queue
+      - .offset:         128
+        .size:           8
+        .value_kind:     hidden_completion_action
+      - .offset:         216
+        .size:           8
+        .value_kind:     hidden_queue_ptr
+    .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 272
+    .max_flat_workgroup_size: 1024
+    .name:           kernel
+    .private_segment_fixed_size: 0
+    .sgpr_count:     39
+    .sgpr_spill_count: 0
+    .symbol:         kernel.kd
+    .uses_dynamic_stack: true
+    .vgpr_count:     32
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-resource-usage-stale-info.s b/lld/test/ELF/amdgpu-resource-usage-stale-info.s
new file mode 100644
index 0000000000000..3e67984051a88
--- /dev/null
+++ b/lld/test/ELF/amdgpu-resource-usage-stale-info.s
@@ -0,0 +1,235 @@
+# REQUIRES: amdgpu
+
+## Stale .amdgpu.info records from losing weak definitions or discarded COMDAT
+## groups must not overwrite the resource usage of the prevailing definition.
+
+# RUN: split-file %s %t
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/kernel.s -o %t/kernel.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/weak-winner.s -o %t/weak-winner.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/weak-stale.s -o %t/weak-stale.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/comdat-winner.s -o %t/comdat-winner.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/comdat-stale.s -o %t/comdat-stale.o
+# RUN: ld.lld %t/kernel.o %t/weak-winner.o %t/weak-stale.o %t/comdat-winner.o %t/comdat-stale.o -o %t/out
+# RUN: llvm-readobj --notes %t/out | FileCheck %s
+
+# CHECK:      .name: kernel
+# CHECK:      .private_segment_fixed_size: 32
+
+#--- kernel.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	kernel
+	.p2align	8
+	.type	kernel, at function
+kernel:
+	s_endpgm
+.Lkernel_end:
+	.size	kernel, .Lkernel_end-kernel
+
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kernel.kd
+	.type	kernel.kd, at object
+	.size	kernel.kd, 64
+	.protected	kernel
+kernel.kd:
+	.long	0
+	.long	0
+	.long	0
+	.long	0
+	.long	0
+	.quad	kernel at rel64-kernel.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	0
+	.long	0
+	.short	0
+	.short	0
+	.long	0
+
+	.text
+	.amdgpu_info kernel
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 4
+		.amdgpu_num_sgpr 4
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 8
+		.amdgpu_wave_size 64
+		.amdgpu_call weak_helper
+		.amdgpu_call comdat_helper
+	.end_amdgpu_info
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 0
+    .max_flat_workgroup_size: 1024
+    .name:           kernel
+    .private_segment_fixed_size: 0
+    .sgpr_count:     0
+    .symbol:         kernel.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- weak-winner.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.weak	weak_helper
+	.p2align	6
+	.type	weak_helper, at function
+weak_helper:
+	s_setpc_b64 s[30:31]
+.Lweak_helper_end:
+	.size	weak_helper, .Lweak_helper_end-weak_helper
+
+	.amdgpu_info weak_helper
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 8
+		.amdgpu_num_sgpr 8
+		.amdgpu_private_segment_size 16
+		.amdgpu_occupancy 8
+		.amdgpu_wave_size 64
+	.end_amdgpu_info
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels: []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- weak-stale.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.weak	weak_helper
+	.p2align	6
+	.type	weak_helper, at function
+weak_helper:
+	s_setpc_b64 s[30:31]
+.Lweak_helper_end:
+	.size	weak_helper, .Lweak_helper_end-weak_helper
+
+	.amdgpu_info weak_helper
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 64
+		.amdgpu_num_sgpr 64
+		.amdgpu_private_segment_size 128
+		.amdgpu_occupancy 4
+		.amdgpu_wave_size 64
+	.end_amdgpu_info
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels: []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- comdat-winner.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.section	.text.comdat_helper,"axG", at progbits,comdat_helper,comdat
+	.globl	comdat_helper
+	.p2align	6
+	.type	comdat_helper, at function
+comdat_helper:
+	s_setpc_b64 s[30:31]
+.Lcomdat_helper_end:
+	.size	comdat_helper, .Lcomdat_helper_end-comdat_helper
+
+	.amdgpu_info comdat_helper
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 12
+		.amdgpu_num_sgpr 12
+		.amdgpu_private_segment_size 32
+		.amdgpu_occupancy 8
+		.amdgpu_wave_size 64
+	.end_amdgpu_info
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels: []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- comdat-stale.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.section	.text.comdat_helper,"axG", at progbits,comdat_helper,comdat
+	.globl	comdat_helper
+	.p2align	6
+	.type	comdat_helper, at function
+comdat_helper:
+	s_setpc_b64 s[30:31]
+.Lcomdat_helper_end:
+	.size	comdat_helper, .Lcomdat_helper_end-comdat_helper
+
+	.amdgpu_info comdat_helper
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 96
+		.amdgpu_num_sgpr 96
+		.amdgpu_private_segment_size 256
+		.amdgpu_occupancy 4
+		.amdgpu_wave_size 64
+	.end_amdgpu_info
+
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels: []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/lld/test/ELF/amdgpu-resource-usage.s b/lld/test/ELF/amdgpu-resource-usage.s
new file mode 100644
index 0000000000000..7f6034675b523
--- /dev/null
+++ b/lld/test/ELF/amdgpu-resource-usage.s
@@ -0,0 +1,400 @@
+# REQUIRES: amdgpu
+
+## Test link-time resource usage propagation for AMDGPU.
+## The linker parses AMDGPU object linking metadata in the `.amdgpu.info` section,
+## propagates resource usage
+## across the call graph, and patches the kernel descriptor and HSA metadata
+## with the propagated values.
+
+# RUN: split-file %s %t
+
+## --- GFX900 test ---
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/a.s -o %t/a.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx900 -filetype=obj %t/b.s -o %t/b.o
+# RUN: ld.lld %t/a.o %t/b.o -o %t/out
+
+## Verify the linker patches HSA metadata after resource propagation.
+# RUN: llvm-readobj --notes %t/out | FileCheck %s
+
+## kernel calls helper (defined in b.ll). After propagation, the kernel's
+## metadata should reflect the max of both functions' register usage.
+# CHECK:      .name: kernel
+# CHECK:      .sgpr_count:
+# CHECK:      .vgpr_count:
+
+## --- GFX90A test ---
+## Test that on GFX90A the linker correctly accounts for extra SGPRs
+## and uses the proper VGPR total computation (GFX90A alignment rule).
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx90a -filetype=obj %t/a90a.s -o %t/a90a.o
+# RUN: llvm-mc -triple=amdgcn-amd-amdhsa -mcpu=gfx90a -filetype=obj %t/b90a.s -o %t/b90a.o
+# RUN: ld.lld %t/a90a.o %t/b90a.o -o %t/out90a
+# RUN: llvm-readobj --notes %t/out90a | FileCheck %s --check-prefix=GFX90A
+
+## After propagation, the kernel should have SGPRs with extras accounted for
+## and VGPR count from the callee.
+# GFX90A:      .name: kernel90a
+# GFX90A:      .sgpr_count:
+# GFX90A:      .vgpr_count:
+
+#--- a.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	kernel                          ; -- Begin function kernel
+	.p2align	8
+	.type	kernel, at function
+kernel:
+	s_endpgm
+.Lfunc_end0:
+	.size	kernel, .Lfunc_end0-kernel
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kernel.kd
+	.type	kernel.kd, at object
+	.size	kernel.kd, 64
+	.protected	kernel
+kernel.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	kernel at rel64-kernel.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	0
+	.long	11469063
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Lkernel.num_vgpr, 32
+	.set .Lkernel.num_agpr, 0
+	.set .Lkernel.numbered_sgpr, 33
+	.set .Lkernel.num_named_barrier, 0
+	.set .Lkernel.private_seg_size, 0
+	.set .Lkernel.uses_vcc, 1
+	.set .Lkernel.uses_flat_scratch, 1
+	.set .Lkernel.has_dyn_sized_stack, 0
+	.set .Lkernel.has_recursion, 0
+	.set .Lkernel.has_indirect_call, 0
+	.amdgpu_info kernel
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_call helper
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           kernel
+    .private_segment_fixed_size: 0
+    .sgpr_count:     39
+    .sgpr_spill_count: 0
+    .symbol:         kernel.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     32
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- b.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx900"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	helper                          ; -- Begin function helper
+	.p2align	6
+	.type	helper, at function
+helper:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	helper, .Lfunc_end0-helper
+	.set .Lhelper.num_vgpr, 0
+	.set .Lhelper.num_agpr, 0
+	.set .Lhelper.numbered_sgpr, 32
+	.set .Lhelper.num_named_barrier, 0
+	.set .Lhelper.private_seg_size, 0
+	.set .Lhelper.uses_vcc, 0
+	.set .Lhelper.uses_flat_scratch, 0
+	.set .Lhelper.has_dyn_sized_stack, 0
+	.set .Lhelper.has_recursion, 0
+	.set .Lhelper.has_indirect_call, 0
+	.amdgpu_info helper
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 0
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 32
+	.set amdgpu.max_num_named_barrier, 0
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:  []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx900
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- a90a.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx90a"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	kernel90a                       ; -- Begin function kernel90a
+	.p2align	8
+	.type	kernel90a, at function
+kernel90a:
+	s_endpgm
+.Lfunc_end0:
+	.size	kernel90a, .Lfunc_end0-kernel90a
+	.section	.rodata,"a", at progbits
+	.p2align	6, 0x0
+	.globl	kernel90a.kd
+	.type	kernel90a.kd, at object
+	.size	kernel90a.kd, 64
+	.protected	kernel90a
+kernel90a.kd:
+	.long	0
+	.long	0
+	.long	264
+	.long	0
+	.quad	kernel90a at rel64-kernel90a.kd
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.byte	0
+	.long	7
+	.long	11469059
+	.long	5020
+	.short	63
+	.short	0
+	.long	0
+	.text
+	.set .Lkernel90a.num_vgpr, 32
+	.set .Lkernel90a.num_agpr, 0
+	.set .Lkernel90a.numbered_sgpr, 33
+	.set .Lkernel90a.num_named_barrier, 0
+	.set .Lkernel90a.private_seg_size, 0
+	.set .Lkernel90a.uses_vcc, 1
+	.set .Lkernel90a.uses_flat_scratch, 1
+	.set .Lkernel90a.has_dyn_sized_stack, 0
+	.set .Lkernel90a.has_recursion, 0
+	.set .Lkernel90a.has_indirect_call, 0
+	.text
+	.amdgpu_info kernel90a
+		.amdgpu_flags 3
+		.amdgpu_num_vgpr 32
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 33
+		.amdgpu_private_segment_size 0
+		.amdgpu_call mfma_helper
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 0
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 0
+	.set amdgpu.max_num_named_barrier, 0
+	.text
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:
+  - .agpr_count:     0
+    .args:
+      - .name:           x
+        .offset:         0
+        .size:           4
+        .value_kind:     by_value
+      - .offset:         8
+        .size:           4
+        .value_kind:     hidden_block_count_x
+      - .offset:         12
+        .size:           4
+        .value_kind:     hidden_block_count_y
+      - .offset:         16
+        .size:           4
+        .value_kind:     hidden_block_count_z
+      - .offset:         20
+        .size:           2
+        .value_kind:     hidden_group_size_x
+      - .offset:         22
+        .size:           2
+        .value_kind:     hidden_group_size_y
+      - .offset:         24
+        .size:           2
+        .value_kind:     hidden_group_size_z
+      - .offset:         26
+        .size:           2
+        .value_kind:     hidden_remainder_x
+      - .offset:         28
+        .size:           2
+        .value_kind:     hidden_remainder_y
+      - .offset:         30
+        .size:           2
+        .value_kind:     hidden_remainder_z
+      - .offset:         48
+        .size:           8
+        .value_kind:     hidden_global_offset_x
+      - .offset:         56
+        .size:           8
+        .value_kind:     hidden_global_offset_y
+      - .offset:         64
+        .size:           8
+        .value_kind:     hidden_global_offset_z
+      - .offset:         72
+        .size:           2
+        .value_kind:     hidden_grid_dims
+      - .offset:         88
+        .size:           8
+        .value_kind:     hidden_hostcall_buffer
+      - .offset:         96
+        .size:           8
+        .value_kind:     hidden_multigrid_sync_arg
+      - .offset:         104
+        .size:           8
+        .value_kind:     hidden_heap_v1
+      - .offset:         112
+        .size:           8
+        .value_kind:     hidden_default_queue
+      - .offset:         120
+        .size:           8
+        .value_kind:     hidden_completion_action
+      - .offset:         208
+        .size:           8
+        .value_kind:     hidden_queue_ptr
+    .group_segment_fixed_size: 0
+    .kernarg_segment_align: 8
+    .kernarg_segment_size: 264
+    .max_flat_workgroup_size: 1024
+    .name:           kernel90a
+    .private_segment_fixed_size: 0
+    .sgpr_count:     39
+    .sgpr_spill_count: 0
+    .symbol:         kernel90a.kd
+    .uses_dynamic_stack: false
+    .vgpr_count:     32
+    .vgpr_spill_count: 0
+    .wavefront_size: 64
+amdhsa.target:   amdgcn-amd-amdhsa--gfx90a
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
+
+#--- b90a.s
+	.amdgcn_target "amdgcn-amd-amdhsa--gfx90a"
+	.amdhsa_code_object_version 6
+	.text
+	.globl	mfma_helper                     ; -- Begin function mfma_helper
+	.p2align	6
+	.type	mfma_helper, at function
+mfma_helper:
+	s_waitcnt vmcnt(0) expcnt(0) lgkmcnt(0)
+	s_setpc_b64 s[30:31]
+.Lfunc_end0:
+	.size	mfma_helper, .Lfunc_end0-mfma_helper
+	.set .Lmfma_helper.num_vgpr, 8
+	.set .Lmfma_helper.num_agpr, 0
+	.set .Lmfma_helper.numbered_sgpr, 32
+	.set .Lmfma_helper.num_named_barrier, 0
+	.set .Lmfma_helper.private_seg_size, 0
+	.set .Lmfma_helper.uses_vcc, 0
+	.set .Lmfma_helper.uses_flat_scratch, 0
+	.set .Lmfma_helper.has_dyn_sized_stack, 0
+	.set .Lmfma_helper.has_recursion, 0
+	.set .Lmfma_helper.has_indirect_call, 0
+	.text
+	.amdgpu_info mfma_helper
+		.amdgpu_flags 0
+		.amdgpu_num_vgpr 8
+		.amdgpu_num_agpr 0
+		.amdgpu_num_sgpr 32
+		.amdgpu_private_segment_size 0
+		.amdgpu_occupancy 4
+	.end_amdgpu_info
+
+	.section	.AMDGPU.gpr_maximums,"", at progbits
+	.set amdgpu.max_num_vgpr, 8
+	.set amdgpu.max_num_agpr, 0
+	.set amdgpu.max_num_sgpr, 32
+	.set amdgpu.max_num_named_barrier, 0
+	.text
+	.section	".note.GNU-stack","", at progbits
+	.amdgpu_metadata
+---
+amdhsa.kernels:  []
+amdhsa.target:   amdgcn-amd-amdhsa--gfx90a
+amdhsa.version:
+  - 1
+  - 2
+...
+
+	.end_amdgpu_metadata
diff --git a/llvm/include/llvm/Support/AMDGPUIsaInfo.h b/llvm/include/llvm/Support/AMDGPUIsaInfo.h
new file mode 100644
index 0000000000000..a719b767ff8d4
--- /dev/null
+++ b/llvm/include/llvm/Support/AMDGPUIsaInfo.h
@@ -0,0 +1,95 @@
+//===- AMDGPUIsaInfo.h - AMDGPU ISA info for cross-component use -*- C++
+//-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Public subset of AMDGPU ISA information needed by the linker (and
+// potentially other cross-component consumers) for subtarget queries and
+// register encoding.
+//
+// These are re-declarations of functions whose implementations live in the
+// AMDGPU target (AMDGPUBaseInfo.cpp). They are duplicated here so that
+// consumers outside the AMDGPU target tree can call them without including
+// target-private headers.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_SUPPORT_AMDGPUISAINFO_H
+#define LLVM_SUPPORT_AMDGPUISAINFO_H
+
+#include "llvm/Support/Compiler.h"
+#include <cstdint>
+#include <optional>
+
+namespace llvm {
+
+class MCSubtargetInfo;
+
+namespace AMDGPU {
+
+/// \returns true if the subtarget has GFX90A instructions (unified VGPR/AGPR).
+LLVM_ABI bool isGFX90A(const MCSubtargetInfo &STI);
+
+/// \returns true if the subtarget is GFX10 or later.
+LLVM_ABI bool isGFX10Plus(const MCSubtargetInfo &STI);
+
+/// \returns true if the subtarget is GFX1250 or later.
+LLVM_ABI bool isGFX1250Plus(const MCSubtargetInfo &STI);
+
+/// Compute total number of VGPRs from arch-VGPR and AGPR counts.
+/// On GFX90A with AGPRs: alignTo(NumArchVGPR, 4) + NumAGPR.
+/// Otherwise: max(NumArchVGPR, NumAGPR).
+LLVM_ABI int getTotalNumVGPRs(bool Has90AInsts, int32_t ArgNumAGPR,
+                              int32_t ArgNumVGPR);
+
+namespace IsaInfo {
+
+/// \returns Number of extra SGPRs implicitly required by the subtarget
+/// when VCC, flat scratch, or XNACK special registers are used.
+LLVM_ABI unsigned getNumExtraSGPRs(const MCSubtargetInfo &STI, bool VCCUsed,
+                                   bool FlatScrUsed, bool XNACKUsed);
+
+/// \returns Number of extra SGPRs implicitly required by the subtarget.
+/// XNACK usage is inferred from the subtarget features.
+LLVM_ABI unsigned getNumExtraSGPRs(const MCSubtargetInfo &STI, bool VCCUsed,
+                                   bool FlatScrUsed);
+
+/// \returns VGPR encoding granularity for the subtarget.
+LLVM_ABI unsigned
+getVGPREncodingGranule(const MCSubtargetInfo &STI,
+                       std::optional<bool> EnableWavefrontSize32);
+
+/// \returns SGPR encoding granularity for the subtarget.
+LLVM_ABI unsigned getSGPREncodingGranule(const MCSubtargetInfo &STI);
+
+/// \returns Encoded number of VGPR blocks (blocks - 1) for the subtarget.
+LLVM_ABI unsigned
+getEncodedNumVGPRBlocks(const MCSubtargetInfo &STI, unsigned NumVGPRs,
+                        std::optional<bool> EnableWavefrontSize32);
+
+/// \returns Number of SGPR blocks (blocks - 1) for the subtarget.
+/// \p NumSGPRs should already include extra SGPR counts.
+LLVM_ABI unsigned getNumSGPRBlocks(const MCSubtargetInfo &STI,
+                                   unsigned NumSGPRs);
+
+/// \returns Allocation granularity for architectural VGPRs (always 4).
+LLVM_ABI unsigned getArchVGPRAllocGranule();
+
+/// \returns Local memory (LDS) size in bytes for the subtarget.
+LLVM_ABI unsigned getLocalMemorySize(const MCSubtargetInfo &STI);
+
+/// \returns true if \p LDSBytes can satisfy \p Occupancy waves per EU using the
+/// object-linking occupancy ABI's 1024-workitem workgroup model.
+LLVM_ABI bool
+isLocalMemorySizeCompatibleWithOccupancy(const MCSubtargetInfo &STI,
+                                         uint64_t LDSBytes, unsigned Occupancy);
+
+} // namespace IsaInfo
+} // namespace AMDGPU
+} // namespace llvm
+
+#endif // LLVM_SUPPORT_AMDGPUISAINFO_H
diff --git a/llvm/include/llvm/Support/AMDGPUObjLinkingInfo.h b/llvm/include/llvm/Support/AMDGPUObjLinkingInfo.h
index ef71e836bfc40..ca545298f42d5 100644
--- a/llvm/include/llvm/Support/AMDGPUObjLinkingInfo.h
+++ b/llvm/include/llvm/Support/AMDGPUObjLinkingInfo.h
@@ -61,6 +61,9 @@ enum class InfoKind : uint8_t {
   /// Occupancy used to compile the function.
   /// [u32]
   INFO_OCCUPANCY = 11,
+  /// Wavefront size (32 or 64) used to compile the function.
+  /// [u32]
+  INFO_WAVE_SIZE = 12,
 };
 
 /// Per-function flags packed into INFO_FLAGS entries.
diff --git a/llvm/lib/Target/AMDGPU/AMDGPUAsmPrinter.cpp b/llvm/lib/Target/AMDGPU/AMDGPUAsmPrinter.cpp
index e9c669d744a5c..ae4207b5ea891 100644
--- a/llvm/lib/Target/AMDGPU/AMDGPUAsmPrinter.cpp
+++ b/llvm/lib/Target/AMDGPU/AMDGPUAsmPrinter.cpp
@@ -958,6 +958,7 @@ bool AMDGPUAsmPrinter::runOnMachineFunction(MachineFunction &MF) {
          /*NumAccVGPR=*/static_cast<uint32_t>(RU.NumAGPR),
          /*PrivateSegmentSize=*/static_cast<uint32_t>(RU.PrivateSegmentSize),
          /*Occupancy=*/Occupancy,
+         /*WaveSize=*/static_cast<uint32_t>(STM.getWavefrontSize()),
          /*UsesVCC=*/RU.UsesVCC,
          /*UsesFlatScratch=*/RU.UsesFlatScratch,
          /*HasDynStack=*/RU.HasDynamicallySizedStack,
diff --git a/llvm/lib/Target/AMDGPU/AMDGPUHSAMetadataStreamer.cpp b/llvm/lib/Target/AMDGPU/AMDGPUHSAMetadataStreamer.cpp
index 793554e9bb432..8a44f808754a4 100644
--- a/llvm/lib/Target/AMDGPU/AMDGPUHSAMetadataStreamer.cpp
+++ b/llvm/lib/Target/AMDGPU/AMDGPUHSAMetadataStreamer.cpp
@@ -514,8 +514,7 @@ MetadataStreamerMsgPackV4::getHSAKernelProps(const MachineFunction &MF,
   // FIXME: The metadata treats the minimum as 16?
   Kern[".kernarg_segment_align"] =
       Kern.getDocument()->getNode(std::max(Align(4), MaxKernArgAlign).value());
-  Kern[".wavefront_size"] =
-      Kern.getDocument()->getNode(STM.getWavefrontSize());
+  Kern[".wavefront_size"] = Kern.getDocument()->getNode(STM.getWavefrontSize());
   DelayedExprs->assignDocNode(Kern[".sgpr_count"], msgpack::Type::UInt,
                               ProgramInfo.NumSGPR);
   DelayedExprs->assignDocNode(Kern[".vgpr_count"], msgpack::Type::UInt,
diff --git a/llvm/lib/Target/AMDGPU/AsmParser/AMDGPUAsmParser.cpp b/llvm/lib/Target/AMDGPU/AsmParser/AMDGPUAsmParser.cpp
index deea1b4b8315b..fd61b7d1381ac 100644
--- a/llvm/lib/Target/AMDGPU/AsmParser/AMDGPUAsmParser.cpp
+++ b/llvm/lib/Target/AMDGPU/AsmParser/AMDGPUAsmParser.cpp
@@ -6936,6 +6936,12 @@ bool AMDGPUAsmParser::ParseDirectiveAMDGPUInfo() {
         return true;
       FI.Occupancy = static_cast<uint32_t>(Val);
       HasScalarAttrs = true;
+    } else if (Dir == "wave_size") {
+      int64_t Val;
+      if (getParser().parseAbsoluteExpression(Val))
+        return true;
+      FI.WaveSize = static_cast<uint32_t>(Val);
+      HasScalarAttrs = true;
     } else if (Dir == "use") {
       StringRef ResName;
       if (getParser().parseIdentifier(ResName))
diff --git a/llvm/lib/Target/AMDGPU/MCTargetDesc/AMDGPUTargetStreamer.cpp b/llvm/lib/Target/AMDGPU/MCTargetDesc/AMDGPUTargetStreamer.cpp
index a72d38def748c..28fac419f4e6f 100644
--- a/llvm/lib/Target/AMDGPU/MCTargetDesc/AMDGPUTargetStreamer.cpp
+++ b/llvm/lib/Target/AMDGPU/MCTargetDesc/AMDGPUTargetStreamer.cpp
@@ -53,7 +53,7 @@ bool AMDGPUTargetStreamer::EmitHSAMetadataV3(StringRef HSAMetadataString) {
   return EmitHSAMetadata(HSAMetadataDoc, false);
 }
 
-StringRef AMDGPUTargetStreamer::getArchNameFromElfMach(unsigned ElfMach) {
+StringRef AMDGPU::getArchNameFromElfMach(unsigned ElfMach) {
   AMDGPU::GPUKind AK;
 
   // clang-format off
@@ -146,6 +146,10 @@ StringRef AMDGPUTargetStreamer::getArchNameFromElfMach(unsigned ElfMach) {
   return getArchNameR600(AK);
 }
 
+StringRef AMDGPUTargetStreamer::getArchNameFromElfMach(unsigned ElfMach) {
+  return AMDGPU::getArchNameFromElfMach(ElfMach);
+}
+
 unsigned AMDGPUTargetStreamer::getElfMach(StringRef GPU) {
   AMDGPU::GPUKind AK = parseArchAMDGCN(GPU);
   if (AK == AMDGPU::GPUKind::GK_NONE)
@@ -760,6 +764,8 @@ void AMDGPUTargetAsmStreamer::emitAMDGPUInfo(
          << '\n';
       if (Info->Occupancy)
         OS << "\t\t.amdgpu_occupancy " << Info->Occupancy << '\n';
+      if (Info->WaveSize)
+        OS << "\t\t.amdgpu_wave_size " << Info->WaveSize << '\n';
     }
     for (MCSymbol *Res : Uses)
       OS << "\t\t.amdgpu_use " << Res->getName() << '\n';
@@ -1236,6 +1242,8 @@ void AMDGPUTargetELFStreamer::emitAMDGPUInfo(
                    Info->PrivateSegmentSize);
       if (Info->Occupancy)
         EmitU32Entry(AMDGPU::InfoKind::INFO_OCCUPANCY, Info->Occupancy);
+      if (Info->WaveSize)
+        EmitU32Entry(AMDGPU::InfoKind::INFO_WAVE_SIZE, Info->WaveSize);
     }
 
     for (MCSymbol *Res : Uses)
diff --git a/llvm/lib/Target/AMDGPU/MCTargetDesc/AMDGPUTargetStreamer.h b/llvm/lib/Target/AMDGPU/MCTargetDesc/AMDGPUTargetStreamer.h
index dd0759c573606..8bcdc2eb7b16e 100644
--- a/llvm/lib/Target/AMDGPU/MCTargetDesc/AMDGPUTargetStreamer.h
+++ b/llvm/lib/Target/AMDGPU/MCTargetDesc/AMDGPUTargetStreamer.h
@@ -24,6 +24,12 @@ class formatted_raw_ostream;
 
 namespace AMDGPU {
 
+/// Map an ELF EF_AMDGPU_MACH_* value to the corresponding GPU name.
+/// Declared here (and defined in AMDGPUTargetStreamer.cpp) so that
+/// cross-component consumers like LLD can call it via forward declaration
+/// without depending on target-private headers.
+StringRef getArchNameFromElfMach(unsigned ElfMach);
+
 struct AMDGPUMCKernelCodeT;
 struct MCKernelDescriptor;
 namespace HSAMD {
@@ -36,6 +42,7 @@ struct FuncInfo {
   uint32_t NumAccVGPR = 0;
   uint32_t PrivateSegmentSize = 0;
   uint32_t Occupancy = 0;
+  uint32_t WaveSize = 0;
   bool UsesVCC = false;
   bool UsesFlatScratch = false;
   bool HasDynStack = false;
diff --git a/llvm/lib/Target/AMDGPU/Utils/AMDGPUBaseInfo.cpp b/llvm/lib/Target/AMDGPU/Utils/AMDGPUBaseInfo.cpp
index d2a9354597252..a822518b2b76c 100644
--- a/llvm/lib/Target/AMDGPU/Utils/AMDGPUBaseInfo.cpp
+++ b/llvm/lib/Target/AMDGPU/Utils/AMDGPUBaseInfo.cpp
@@ -25,6 +25,7 @@
 #include "llvm/MC/MCInstrInfo.h"
 #include "llvm/MC/MCRegisterInfo.h"
 #include "llvm/MC/MCSubtargetInfo.h"
+#include "llvm/Support/AMDGPUIsaInfo.h"
 #include "llvm/Support/CommandLine.h"
 #include "llvm/TargetParser/AMDGPUTargetParser.h"
 #include <optional>
@@ -1570,6 +1571,26 @@ unsigned getAllocatedNumVGPRBlocks(const MCSubtargetInfo &STI,
       NumVGPRs,
       getVGPRAllocGranule(STI, DynamicVGPRBlockSize, EnableWavefrontSize32));
 }
+
+bool isLocalMemorySizeCompatibleWithOccupancy(const MCSubtargetInfo &STI,
+                                              uint64_t LDSBytes,
+                                              unsigned Occupancy) {
+  if (Occupancy == 0 || Occupancy > getMaxWavesPerEU(STI))
+    return false;
+
+  uint64_t Granularity = uint64_t(getLdsDwGranularity(STI)) * sizeof(uint32_t);
+  uint64_t AlignedLDSBytes = alignTo(LDSBytes, Granularity);
+  if (AlignedLDSBytes > getAddressableLocalMemorySize(STI))
+    return false;
+
+  uint64_t WavesPerWorkgroup =
+      getWavesPerWorkGroup(STI, getMaxFlatWorkGroupSize());
+  uint64_t RequiredWavesPerCU = uint64_t(Occupancy) * getEUsPerCU(STI);
+  uint64_t WorkGroupsPerCU =
+      std::max<uint64_t>(divideCeil(RequiredWavesPerCU, WavesPerWorkgroup), 1);
+
+  return AlignedLDSBytes <= getLocalMemorySize(STI) / WorkGroupsPerCU;
+}
 } // end namespace IsaInfo
 
 void initDefaultAMDKernelCodeT(AMDGPUMCKernelCodeT &KernelCode,



More information about the llvm-branch-commits mailing list