[llvm] [Debug] Add support for multi-level line support (PR #211390)

Alexander Yermolovich via llvm-commits llvm-commits at lists.llvm.org
Tue Aug 11 13:17:46 PDT 2026


https://github.com/ayermolo updated https://github.com/llvm/llvm-project/pull/211390

>From 9fb903d453307c4003fbf026cf0887b6bbf7bea6 Mon Sep 17 00:00:00 2001
From: Alexander Yermolovich <ayermolovich at nvidia.com>
Date: Tue, 11 Aug 2026 19:53:45 +0000
Subject: [PATCH 1/3] [DebugInfo] Add DILayerLoc/DILayerLocList and DILocation
 irlayers operand

Programs lowered through intermediate IRs (for example a tile IR lowered to a
GPU IR) currently lose their position in those IRs by the time they reach a
backend: a DILocation records only the original source coordinate. This adds
that missing dimension so a debugger can step through the intermediate text a
program was actually compiled from.

DILayerLoc holds one intermediate coordinate (kind string, DIFile, line,
column). DILayerLocList holds a uniqued list of them, one per IR level.
DILocation gains an optional trailing `irlayers` operand pointing at a list,
guarded by a HasIRLayers bit taken from the atom-group bitfield. That narrows
atomGroup from 61 to 60 bits; the value comes from a monotonic per-context
counter so the range is unreachable in practice, and getImpl now clamps
out-of-range values to 0 rather than letting them truncate into a different
valid group.

Layers are independent of inlinedAt: a location may carry either, both, or
neither. Verification is structural only - a kind string and a DIFile - so
producers that omit a checksum still verify clean. Operand order is preserved
and is part of a list's identity, but LLVM assigns it no meaning.

Serialization is included here rather than split out because both the bitcode
writer and the DirectX writer dispatch over Metadata.def leaves, so they stop
compiling the moment the nodes are declared; a writer without its reader would
also leave a commit that cannot round-trip its own output. Textual IR, bitcode
and MIR all carry the new operand.
---
 llvm/docs/KeyInstructionsDebugInfo.md         |   2 +-
 llvm/docs/LangRef.md                          |  49 +++
 llvm/include/llvm-c/DebugInfo.h               |   2 +
 llvm/include/llvm/Bitcode/LLVMBitCodes.h      |   2 +
 llvm/include/llvm/IR/DebugInfoMetadata.h      | 174 +++++++-
 llvm/include/llvm/IR/DebugLoc.h               |   7 +-
 llvm/include/llvm/IR/Metadata.def             |   2 +
 llvm/lib/AsmParser/LLParser.cpp               |  44 +-
 llvm/lib/Bitcode/Reader/BitcodeReader.cpp     |   9 +-
 llvm/lib/Bitcode/Reader/MetadataLoader.cpp    |  48 ++-
 llvm/lib/Bitcode/Writer/BitcodeWriter.cpp     |  30 ++
 llvm/lib/CodeGen/MIRParser/MIParser.cpp       |  13 +-
 llvm/lib/IR/AsmWriter.cpp                     |  23 +
 llvm/lib/IR/DebugInfoMetadata.cpp             | 131 +++++-
 llvm/lib/IR/DebugLoc.cpp                      |  12 +-
 llvm/lib/IR/LLVMContextImpl.h                 |  64 ++-
 llvm/lib/IR/Verifier.cpp                      |  16 +
 .../DirectX/DXILWriter/DXILBitcodeWriter.cpp  |   8 +
 .../intermediate-loc-metadata-location.ll     |  58 +++
 .../Bitcode/intermediate-loc-roundtrip.ll     |  57 +++
 .../instructions-debug-location-irlayers.mir  |  64 +++
 llvm/test/Verifier/intermediate-layerloc.ll   |  37 ++
 llvm/unittests/IR/DebugInfoTest.cpp           | 402 ++++++++++++++++++
 23 files changed, 1194 insertions(+), 60 deletions(-)
 create mode 100644 llvm/test/Bitcode/intermediate-loc-metadata-location.ll
 create mode 100644 llvm/test/Bitcode/intermediate-loc-roundtrip.ll
 create mode 100644 llvm/test/CodeGen/MIR/X86/instructions-debug-location-irlayers.mir
 create mode 100644 llvm/test/Verifier/intermediate-layerloc.ll

diff --git a/llvm/docs/KeyInstructionsDebugInfo.md b/llvm/docs/KeyInstructionsDebugInfo.md
index 22f83f2b0a381..0ea9b84c180f1 100644
--- a/llvm/docs/KeyInstructionsDebugInfo.md
+++ b/llvm/docs/KeyInstructionsDebugInfo.md
@@ -45,7 +45,7 @@ From the perspective of a source-level debugger user:
 
 Details:
 
-1. *The metadata* - The two new `DILocation` fields are `atomGroup` and `atomRank` and are both are unsigned integers. `atomGroup` is 61 bits and `atomRank` 3 bits. Instructions in the same function with the same `(atomGroup, inlinedAt)` pair are part of the same source atom. `atomRank` determines `is_stmt` preference within that group, where a lower number is higher precedence. Higher rank instructions act as "backup" `is_stmt` locations, providing good fallback locations if/when the primary candidate gets optimized away. The default values of 0 indicate the instruction isn’t interesting - it's not an `is_stmt` candidate. If `keyInstructions` in `DISubprogram` is false (default) then the new `DILocation` metadata is ignored for the function (including inlined instances) when emitting DWARF.
+1. *The metadata* - The two new `DILocation` fields are `atomGroup` and `atomRank` and are both are unsigned integers. `atomGroup` is 60 bits and `atomRank` 3 bits; the remaining bit of the 64-bit field is `DILocation`'s `irlayers` flag. A group that does not fit in 60 bits is clamped to 0, so an out-of-range value means "not interesting" rather than silently aliasing another group. Instructions in the same function with the same `(atomGroup, inlinedAt)` pair are part of the same source atom. `atomRank` determines `is_stmt` preference within that group, where a lower number is higher precedence. Higher rank instructions act as "backup" `is_stmt` locations, providing good fallback locations if/when the primary candidate gets optimized away. The default values of 0 indicate the instruction isn’t interesting - it's not an `is_stmt` candidate. If `keyInstructions` in `DISubprogram` is false (default) then the new `DILocation` metadata is ignored for the function (including inlined instances) when emitting DWARF.
 
 2. *Clang annotates key instructions* with the new metadata. Variable assignments (stores, memory intrinsics), control flow (branches and their conditions, some unconditional branches), and exception handling instructions are annotated. Calls are ignored as they're unconditionally marked `is_stmt`.
 
diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md
index 3a616e8a29fcf..c1642705ab481 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -7170,6 +7170,55 @@ mandatory, and points at an {ref}`DILexicalBlockFile`, an
 !0 = !DILocation(line: 2900, column: 42, scope: !1, inlinedAt: !2)
 ```
 
+The optional `irlayers:` field points at a {ref}`DILayerLocList`, giving the
+instruction's position in one or more intermediate IRs it was lowered through, in
+addition to its primary source position. It is independent of `inlinedAt:`; a
+location may have either, both, or neither.
+
+```text
+!0 = !DILocation(line: 2900, column: 42, scope: !1, irlayers: !3)
+```
+
+(DILayerLoc)=
+
+##### DILayerLoc
+
+`DILayerLoc` nodes represent a source position in one intermediate IR level that
+a program was lowered through — for example a tile IR or an MLIR module produced
+part-way through compilation. The `kind:` field names the level and the `file:`
+field points at a {ref}`DIFile` for it; both are mandatory. `line:` and
+`column:` are the position within that file.
+
+Unlike a {ref}`DILocation`, a `DILayerLoc` has no scope and no inlined-at
+context: it is a bare coordinate in a file, not a location in a scope tree.
+
+```text
+!0 = !DILayerLoc(line: 100, column: 1, file: !1, kind: "tile ir")
+```
+
+(DILayerLocList)=
+
+##### DILayerLocList
+
+`DILayerLocList` nodes hold a list of {ref}`DILayerLoc` operands, and are
+referenced by a {ref}`DILocation`'s `irlayers:` field.
+
+Operand order is preserved and is part of the node's identity — two lists with
+the same entries in a different order are different nodes — and a consumer sees
+the entries in that order. LLVM itself attaches no meaning to the order: it does
+not define which level comes first and does not check any particular arrangement,
+so any convention (such as listing levels in lowering order) is an agreement
+between a producer and its consumer.
+
+Both node types are uniqued, so instructions sharing a position at some level
+share the corresponding node.
+
+```text
+!0 = !DILayerLocList(!1, !2)
+!1 = !DILayerLoc(line: 100, column: 1, file: !3, kind: "tile ir")
+!2 = !DILayerLoc(line: 7, column: 3, file: !4, kind: "gpu ir")
+```
+
 (DILocalVariable)=
 
 ##### DILocalVariable
diff --git a/llvm/include/llvm-c/DebugInfo.h b/llvm/include/llvm-c/DebugInfo.h
index 155cbe32ee15e..21b1e021ff310 100644
--- a/llvm/include/llvm-c/DebugInfo.h
+++ b/llvm/include/llvm-c/DebugInfo.h
@@ -211,6 +211,8 @@ enum {
   LLVMDIAssignIDMetadataKind,
   LLVMDISubrangeTypeMetadataKind,
   LLVMDIFixedPointTypeMetadataKind,
+  LLVMDILayerLocMetadataKind,
+  LLVMDILayerLocListMetadataKind,
 };
 typedef unsigned LLVMMetadataKind;
 
diff --git a/llvm/include/llvm/Bitcode/LLVMBitCodes.h b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
index 358f9a65a80af..3f7db3456ea78 100644
--- a/llvm/include/llvm/Bitcode/LLVMBitCodes.h
+++ b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
@@ -400,6 +400,8 @@ enum MetadataCodes {
   METADATA_ASSIGN_ID = 47,        // [distinct, ...]
   METADATA_SUBRANGE_TYPE = 48,    // [distinct, ...]
   METADATA_FIXED_POINT_TYPE = 49, // [distinct, ...]
+  METADATA_LAYERLOC = 50,         // [distinct, line, column, file, kind]
+  METADATA_LAYERLOCLIST = 51,     // [distinct, n x layerloc]
 };
 
 // The constants block (CONSTANTS_BLOCK_ID) describes emission for each
diff --git a/llvm/include/llvm/IR/DebugInfoMetadata.h b/llvm/include/llvm/IR/DebugInfoMetadata.h
index 33a1a6e482555..ab6ad11913495 100644
--- a/llvm/include/llvm/IR/DebugInfoMetadata.h
+++ b/llvm/include/llvm/IR/DebugInfoMetadata.h
@@ -2658,6 +2658,114 @@ class DISubprogram : public DILocalScope {
   }
 };
 
+/// A single intermediate-IR layer location.
+///
+/// One source coordinate in an intermediate IR level (e.g. TileIR, MLIR) that
+/// sits between the high-level source and the final LLVM IR. Lightweight: it
+/// references its \a DIFile directly (no scope, no discriminator) and stores
+/// line/column in the free Metadata subclass-data slots. Grouped behind a
+/// \a DILayerLocList on \a DILocation's optional `irlayers` operand.
+///
+/// Uses the SubclassData16 and SubclassData32 Metadata slots.
+class DILayerLoc : public MDNode {
+  friend class LLVMContextImpl;
+  friend class MDNode;
+
+  DILayerLoc(LLVMContext &C, StorageType Storage, unsigned Line,
+             unsigned Column, ArrayRef<Metadata *> Ops)
+      : MDNode(C, DILayerLocKind, Storage, Ops) {
+    assert(Ops.size() == 2 && "Expected {kind, file}");
+    assert(Column < (1u << 16) && "Expected 16-bit column");
+    SubclassData32 = Line;
+    SubclassData16 = Column;
+  }
+  ~DILayerLoc() { dropAllReferences(); }
+
+  static DILayerLoc *getImpl(LLVMContext &Context, MDString *Kind,
+                             Metadata *File, unsigned Line, unsigned Column,
+                             StorageType Storage, bool ShouldCreate = true);
+
+  TempDILayerLoc cloneImpl() const {
+    return getTemporary(getContext(), getRawKind(), getRawFile(), getLine(),
+                        getColumn());
+  }
+
+public:
+  DEFINE_MDNODE_GET(DILayerLoc,
+                    (MDString * Kind, Metadata *File, unsigned Line,
+                     unsigned Column),
+                    (Kind, File, Line, Column))
+
+  TempDILayerLoc clone() const { return cloneImpl(); }
+
+  unsigned getLine() const { return SubclassData32; }
+  unsigned getColumn() const { return SubclassData16; }
+
+  MDString *getRawKind() const {
+    return cast_if_present<MDString>(getOperand(0));
+  }
+  StringRef getKind() const {
+    if (MDString *K = getRawKind())
+      return K->getString();
+    return StringRef();
+  }
+  Metadata *getRawFile() const { return getOperand(1); }
+  DIFile *getFile() const { return cast_if_present<DIFile>(getRawFile()); }
+
+  static bool classof(const Metadata *MD) {
+    return MD->getMetadataID() == DILayerLocKind;
+  }
+};
+
+/// A uniqued list of \a DILayerLoc entries.
+///
+/// The container node behind \a DILocation's optional `irlayers` operand:
+/// essentially an \a MDTuple of \a DILayerLoc refs with typed accessors and its
+/// own metadata kind, so consumers can type-check it with
+/// `isa<DILayerLocList>`. Operand order is preserved and is part of the node's
+/// identity, but LLVM assigns it no meaning.
+class DILayerLocList : public MDNode {
+  friend class LLVMContextImpl;
+  friend class MDNode;
+
+  DILayerLocList(LLVMContext &C, StorageType Storage, unsigned Hash,
+                 ArrayRef<Metadata *> Ops)
+      : MDNode(C, DILayerLocListKind, Storage, Ops) {
+    setHash(Hash);
+  }
+  ~DILayerLocList() { dropAllReferences(); }
+
+  void setHash(unsigned Hash) { SubclassData32 = Hash; }
+  void recalculateHash();
+
+  static DILayerLocList *getImpl(LLVMContext &Context,
+                                 ArrayRef<Metadata *> Layers,
+                                 StorageType Storage, bool ShouldCreate = true);
+
+  TempDILayerLocList cloneImpl() const {
+    return getTemporary(getContext(), SmallVector<Metadata *, 4>(operands()));
+  }
+
+public:
+  /// Get the operand hash (used by the MDNodeOpsKey uniquing key).
+  unsigned getHash() const { return SubclassData32; }
+
+  DEFINE_MDNODE_GET(DILayerLocList, (ArrayRef<Metadata *> Layers), (Layers))
+
+  TempDILayerLocList clone() const { return cloneImpl(); }
+
+  unsigned getNumLayers() const { return getNumOperands(); }
+  DILayerLoc *getLayer(unsigned I) const {
+    return cast_if_present<DILayerLoc>(getOperand(I));
+  }
+  using layer_iterator = MDNode::op_iterator;
+  iterator_range<layer_iterator> layers() const { return operands(); }
+
+  static bool classof(const Metadata *MD) {
+    return MD->getMetadataID() == DILayerLocListKind;
+  }
+};
+
 /// Debug location.
 ///
 /// A debug location in source code, used for debug info and otherwise.
@@ -2668,34 +2776,40 @@ class DISubprogram : public DILocalScope {
 class DILocation : public MDNode {
   friend class LLVMContextImpl;
   friend class MDNode;
-  uint64_t AtomGroup : 61;
+  uint64_t AtomGroup : 60;
   uint64_t AtomRank : 3;
+  // Disambiguates the two optional trailing operands, layout
+  // [scope, (inlinedAt?), (irlayers?)]: irlayers is always last when present.
+  uint64_t HasIRLayers : 1;
 
   DILocation(LLVMContext &C, StorageType Storage, unsigned Line,
              unsigned Column, uint64_t AtomGroup, uint8_t AtomRank,
-             ArrayRef<Metadata *> MDs, bool ImplicitCode);
+             bool HasIRLayers, ArrayRef<Metadata *> MDs, bool ImplicitCode);
   ~DILocation() { dropAllReferences(); }
 
-  LLVM_ABI static DILocation *
-  getImpl(LLVMContext &Context, unsigned Line, unsigned Column, Metadata *Scope,
-          Metadata *InlinedAt, bool ImplicitCode, uint64_t AtomGroup,
-          uint8_t AtomRank, StorageType Storage, bool ShouldCreate = true);
+  LLVM_ABI static DILocation *getImpl(LLVMContext &Context, unsigned Line,
+                                      unsigned Column, Metadata *Scope,
+                                      Metadata *InlinedAt, bool ImplicitCode,
+                                      uint64_t AtomGroup, uint8_t AtomRank,
+                                      Metadata *IRLayers, StorageType Storage,
+                                      bool ShouldCreate = true);
   static DILocation *getImpl(LLVMContext &Context, unsigned Line,
                              unsigned Column, DILocalScope *Scope,
                              DILocation *InlinedAt, bool ImplicitCode,
                              uint64_t AtomGroup, uint8_t AtomRank,
-                             StorageType Storage, bool ShouldCreate = true) {
+                             Metadata *IRLayers, StorageType Storage,
+                             bool ShouldCreate = true) {
     return getImpl(Context, Line, Column, static_cast<Metadata *>(Scope),
                    static_cast<Metadata *>(InlinedAt), ImplicitCode, AtomGroup,
-                   AtomRank, Storage, ShouldCreate);
+                   AtomRank, IRLayers, Storage, ShouldCreate);
   }
 
   TempDILocation cloneImpl() const {
-    // Get the raw scope/inlinedAt since it is possible to invoke this on
-    // a DILocation containing temporary metadata.
+    // Get the raw scope/inlinedAt/irlayers since it is possible to invoke this
+    // on a DILocation containing temporary metadata.
     return getTemporary(getContext(), getLine(), getColumn(), getRawScope(),
                         getRawInlinedAt(), isImplicitCode(), getAtomGroup(),
-                        getAtomRank());
+                        getAtomRank(), getRawIRLayers());
   }
 
 public:
@@ -2706,7 +2820,8 @@ class DILocation : public MDNode {
     if (!getAtomGroup() && !getAtomRank())
       return this;
     return get(getContext(), getLine(), getColumn(), getScope(), getInlinedAt(),
-               isImplicitCode());
+               isImplicitCode(), /*AtomGroup=*/0, /*AtomRank=*/0,
+               getRawIRLayers());
   }
 
   // Disallow replacing operands.
@@ -2715,15 +2830,17 @@ class DILocation : public MDNode {
   DEFINE_MDNODE_GET(DILocation,
                     (unsigned Line, unsigned Column, Metadata *Scope,
                      Metadata *InlinedAt = nullptr, bool ImplicitCode = false,
-                     uint64_t AtomGroup = 0, uint8_t AtomRank = 0),
+                     uint64_t AtomGroup = 0, uint8_t AtomRank = 0,
+                     Metadata *IRLayers = nullptr),
                     (Line, Column, Scope, InlinedAt, ImplicitCode, AtomGroup,
-                     AtomRank))
+                     AtomRank, IRLayers))
   DEFINE_MDNODE_GET(DILocation,
                     (unsigned Line, unsigned Column, DILocalScope *Scope,
                      DILocation *InlinedAt = nullptr, bool ImplicitCode = false,
-                     uint64_t AtomGroup = 0, uint8_t AtomRank = 0),
+                     uint64_t AtomGroup = 0, uint8_t AtomRank = 0,
+                     Metadata *IRLayers = nullptr),
                     (Line, Column, Scope, InlinedAt, ImplicitCode, AtomGroup,
-                     AtomRank))
+                     AtomRank, IRLayers))
 
   /// Return a (temporary) clone of this.
   TempDILocation clone() const { return cloneImpl(); }
@@ -2945,11 +3062,32 @@ class DILocation : public MDNode {
 
   Metadata *getRawScope() const { return getOperand(0); }
   Metadata *getRawInlinedAt() const {
-    if (getNumOperands() == 2)
+    // Layout: [scope, (inlinedAt?), (irlayers?)]. irlayers, when present, is
+    // always the last operand; discount it before the inlinedAt count trick.
+    unsigned NonLayerOps = getNumOperands() - (HasIRLayers ? 1 : 0);
+    if (NonLayerOps == 2)
       return getOperand(1);
     return nullptr;
   }
 
+  /// The optional intermediate-IR layer list (\a DILayerLocList), or null.
+  /// Raw form: returns the operand without casting, so it is safe to call
+  /// before forward-ref resolution (the operand may still be a placeholder).
+  Metadata *getRawIRLayers() const {
+    return HasIRLayers ? getOperand(getNumOperands() - 1) : nullptr;
+  }
+  DILayerLocList *getIRLayers() const {
+    return cast_if_present<DILayerLocList>(getRawIRLayers());
+  }
+  unsigned getNumLayers() const {
+    DILayerLocList *L = getIRLayers();
+    return L ? L->getNumLayers() : 0;
+  }
+  DILayerLoc *getLayer(unsigned I) const {
+    DILayerLocList *L = getIRLayers();
+    return L ? L->getLayer(I) : nullptr;
+  }
+
   static bool classof(const Metadata *MD) {
     return MD->getMetadataID() == DILocationKind;
   }
@@ -3103,7 +3241,7 @@ DILocation::cloneWithDiscriminator(unsigned Discriminator) const {
       DILexicalBlockFile::get(getContext(), Scope, getFile(), Discriminator);
   return DILocation::get(getContext(), getLine(), getColumn(), NewScope,
                          getInlinedAt(), isImplicitCode(), getAtomGroup(),
-                         getAtomRank());
+                         getAtomRank(), getRawIRLayers());
 }
 
 unsigned DILocation::getBaseDiscriminator() const {
diff --git a/llvm/include/llvm/IR/DebugLoc.h b/llvm/include/llvm/IR/DebugLoc.h
index 34e6aadd83cae..c9a5149c80940 100644
--- a/llvm/include/llvm/IR/DebugLoc.h
+++ b/llvm/include/llvm/IR/DebugLoc.h
@@ -246,13 +246,18 @@ class DebugLoc {
       return true;
     return ((bool)*this == (bool)Other) && getLine() == Other.getLine() &&
            getCol() == Other.getCol() && getScope() == Other.getScope() &&
-           getInlinedAt() == Other.getInlinedAt();
+           getInlinedAt() == Other.getInlinedAt() &&
+           getRawIRLayers() == Other.getRawIRLayers();
   }
 
   LLVM_ABI unsigned getLine() const;
   LLVM_ABI unsigned getCol() const;
   LLVM_ABI MDNode *getScope() const;
   LLVM_ABI DILocation *getInlinedAt() const;
+  /// The raw intermediate-IR layer list (\a DILayerLocList) of the underlying
+  /// location, or null. Out-of-line so this header need not see DILocation's
+  /// definition.
+  LLVM_ABI MDNode *getRawIRLayers() const;
 
   /// Get the fully inlined-at scope for a DebugLoc.
   ///
diff --git a/llvm/include/llvm/IR/Metadata.def b/llvm/include/llvm/IR/Metadata.def
index 511bf48707f00..80817892d0d99 100644
--- a/llvm/include/llvm/IR/Metadata.def
+++ b/llvm/include/llvm/IR/Metadata.def
@@ -81,6 +81,8 @@ HANDLE_METADATA_LEAF(DIArgList)
 HANDLE_MDNODE_BRANCH(MDNode)
 HANDLE_MDNODE_LEAF_UNIQUABLE(MDTuple)
 HANDLE_SPECIALIZED_MDNODE_LEAF_UNIQUABLE(DILocation)
+HANDLE_SPECIALIZED_MDNODE_LEAF_UNIQUABLE(DILayerLoc)
+HANDLE_SPECIALIZED_MDNODE_LEAF_UNIQUABLE(DILayerLocList)
 HANDLE_SPECIALIZED_MDNODE_LEAF_UNIQUABLE(DIExpression)
 HANDLE_SPECIALIZED_MDNODE_LEAF_UNIQUABLE(DIGlobalVariableExpression)
 HANDLE_SPECIALIZED_MDNODE_BRANCH(DINode)
diff --git a/llvm/lib/AsmParser/LLParser.cpp b/llvm/lib/AsmParser/LLParser.cpp
index edff818b3b152..34d583820227f 100644
--- a/llvm/lib/AsmParser/LLParser.cpp
+++ b/llvm/lib/AsmParser/LLParser.cpp
@@ -5784,13 +5784,49 @@ bool LLParser::parseDILocation(MDNode *&Result, bool IsDistinct) {
   OPTIONAL(inlinedAt, MDField, );                                              \
   OPTIONAL(isImplicitCode, MDBoolField, (false));                              \
   OPTIONAL(atomGroup, MDUnsignedField, (0, UINT64_MAX));                       \
-  OPTIONAL(atomRank, MDUnsignedField, (0, UINT8_MAX));
+  OPTIONAL(atomRank, MDUnsignedField, (0, UINT8_MAX));                         \
+  OPTIONAL(irlayers, MDField, );
   PARSE_MD_FIELDS();
 #undef VISIT_MD_FIELDS
 
-  Result = GET_OR_DISTINCT(
-      DILocation, (Context, line.Val, column.Val, scope.Val, inlinedAt.Val,
-                   isImplicitCode.Val, atomGroup.Val, atomRank.Val));
+  Result =
+      GET_OR_DISTINCT(DILocation, (Context, line.Val, column.Val, scope.Val,
+                                   inlinedAt.Val, isImplicitCode.Val,
+                                   atomGroup.Val, atomRank.Val, irlayers.Val));
+  return false;
+}
+
+bool LLParser::parseDILayerLoc(MDNode *&Result, bool IsDistinct) {
+#define VISIT_MD_FIELDS(OPTIONAL, REQUIRED)                                    \
+  OPTIONAL(line, LineField, );                                                 \
+  OPTIONAL(column, ColumnField, );                                             \
+  REQUIRED(file, MDField, (/* AllowNull */ false));                            \
+  REQUIRED(kind, MDStringField, );
+  PARSE_MD_FIELDS();
+#undef VISIT_MD_FIELDS
+
+  Result = GET_OR_DISTINCT(DILayerLoc,
+                           (Context, kind.Val, file.Val, line.Val, column.Val));
+  return false;
+}
+
+bool LLParser::parseDILayerLocList(MDNode *&Result, bool IsDistinct) {
+  // ::= !DILayerLocList(!a, !b, ...)
+  Lex.Lex(); // eat the '!DILayerLocList' type name
+  if (parseToken(lltok::lparen, "expected '(' here"))
+    return true;
+  SmallVector<Metadata *, 4> Layers;
+  if (!EatIfPresent(lltok::rparen)) {
+    do {
+      Metadata *MD;
+      if (parseMetadata(MD, nullptr))
+        return true;
+      Layers.push_back(MD);
+    } while (EatIfPresent(lltok::comma));
+    if (parseToken(lltok::rparen, "expected ')' here"))
+      return true;
+  }
+  Result = GET_OR_DISTINCT(DILayerLocList, (Context, Layers));
   return false;
 }
 
diff --git a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
index ac61ede6395af..b093854488ae0 100644
--- a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
+++ b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
@@ -5241,8 +5241,8 @@ Error BitcodeReader::parseFunctionBody(Function *F) {
       unsigned Line = Record[0], Col = Record[1];
       unsigned ScopeID = Record[2], IAID = Record[3];
       bool isImplicitCode = Record.size() >= 5 && Record[4];
-      uint64_t AtomGroup = Record.size() == 7 ? Record[5] : 0;
-      uint8_t AtomRank = Record.size() == 7 ? Record[6] : 0;
+      uint64_t AtomGroup = Record.size() >= 7 ? Record[5] : 0;
+      uint8_t AtomRank = Record.size() >= 7 ? Record[6] : 0;
 
       MDNode *Scope = nullptr, *IA = nullptr;
       if (ScopeID) {
@@ -5257,9 +5257,12 @@ Error BitcodeReader::parseFunctionBody(Function *F) {
         if (!IA)
           return error("Invalid debug loc record");
       }
+      Metadata *IRLayers = nullptr;
+      if (Record.size() >= 8 && Record[7])
+        IRLayers = MDLoader->getMetadataFwdRefOrLoad(Record[7] - 1);
 
       LastLoc = DILocation::get(Scope->getContext(), Line, Col, Scope, IA,
-                                isImplicitCode, AtomGroup, AtomRank);
+                                isImplicitCode, AtomGroup, AtomRank, IRLayers);
       I->setDebugLoc(LastLoc);
       I = nullptr;
       continue;
diff --git a/llvm/lib/Bitcode/Reader/MetadataLoader.cpp b/llvm/lib/Bitcode/Reader/MetadataLoader.cpp
index 8b7beb1a8ff9e..4fa47f7db0e21 100644
--- a/llvm/lib/Bitcode/Reader/MetadataLoader.cpp
+++ b/llvm/lib/Bitcode/Reader/MetadataLoader.cpp
@@ -1472,8 +1472,12 @@ Error MetadataLoader::MetadataLoaderImpl::parseOneMetadata(
     break;
   }
   case bitc::METADATA_LOCATION: {
-    // 5: inlinedAt, 6: isImplicit, 8: Key Instructions fields.
-    if (Record.size() != 5 && Record.size() != 6 && Record.size() != 8)
+    // Defined shapes: 5 (through inlinedAt), 6 (isImplicit), 8 (adds the Key
+    // Instructions fields), 9 (adds irlayers). Anything longer is a record from
+    // a newer writer and its trailing fields are ignored, which is what makes
+    // this encoding append-only; anything shorter, or a 7 (an atomGroup with no
+    // atomRank), is a shape no writer produces.
+    if (Record.size() < 5 || Record.size() == 7)
       return error("Invalid record");
 
     IsDistinct = Record[0];
@@ -1482,15 +1486,47 @@ Error MetadataLoader::MetadataLoaderImpl::parseOneMetadata(
     Metadata *Scope = getMD(Record[3]);
     Metadata *InlinedAt = getMDOrNull(Record[4]);
     bool ImplicitCode = Record.size() >= 6 && Record[5];
-    uint64_t AtomGroup = Record.size() == 8 ? Record[6] : 0;
-    uint8_t AtomRank = Record.size() == 8 ? Record[7] : 0;
+    uint64_t AtomGroup = Record.size() >= 8 ? Record[6] : 0;
+    uint8_t AtomRank = Record.size() >= 8 ? Record[7] : 0;
+    Metadata *IRLayers = Record.size() >= 9 ? getMDOrNull(Record[8]) : nullptr;
     MetadataList.assignValue(
-        GET_OR_DISTINCT(DILocation, (Context, Line, Column, Scope, InlinedAt,
-                                     ImplicitCode, AtomGroup, AtomRank)),
+        GET_OR_DISTINCT(DILocation,
+                        (Context, Line, Column, Scope, InlinedAt, ImplicitCode,
+                         AtomGroup, AtomRank, IRLayers)),
         NextMetadataNo);
     NextMetadataNo++;
     break;
   }
+  case bitc::METADATA_LAYERLOC: {
+    if (Record.size() != 5)
+      return error("Invalid record");
+
+    IsDistinct = Record[0];
+    unsigned Line = Record[1];
+    unsigned Column = Record[2];
+    Metadata *File = getMD(Record[3]);
+    // Read the kind opaquely and let the verifier report a bad type, as the
+    // other DI readers do: an unchecked cast would assert on malformed bitcode.
+    MDString *Kind = dyn_cast_if_present<MDString>(getMD(Record[4]));
+    MetadataList.assignValue(
+        GET_OR_DISTINCT(DILayerLoc, (Context, Kind, File, Line, Column)),
+        NextMetadataNo);
+    NextMetadataNo++;
+    break;
+  }
+  case bitc::METADATA_LAYERLOCLIST: {
+    if (Record.empty())
+      return error("Invalid record");
+
+    IsDistinct = Record[0];
+    SmallVector<Metadata *, 4> Elts;
+    for (unsigned I = 1, E = Record.size(); I != E; ++I)
+      Elts.push_back(getMDOrNull(Record[I]));
+    MetadataList.assignValue(GET_OR_DISTINCT(DILayerLocList, (Context, Elts)),
+                             NextMetadataNo);
+    NextMetadataNo++;
+    break;
+  }
   case bitc::METADATA_GENERIC_DEBUG: {
     if (Record.size() < 4)
       return error("Invalid record");
diff --git a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
index 571336c217797..4edba342fed48 100644
--- a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
+++ b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
@@ -340,6 +340,10 @@ class ModuleBitcodeWriter : public ModuleBitcodeWriterBase {
   unsigned createDILocationAbbrev();
   void writeDILocation(const DILocation *N, SmallVectorImpl<uint64_t> &Record,
                        unsigned &Abbrev);
+  void writeDILayerLoc(const DILayerLoc *N, SmallVectorImpl<uint64_t> &Record,
+                       unsigned Abbrev);
+  void writeDILayerLocList(const DILayerLocList *N,
+                           SmallVectorImpl<uint64_t> &Record, unsigned Abbrev);
   unsigned createGenericDINodeAbbrev();
   void writeGenericDINode(const GenericDINode *N,
                           SmallVectorImpl<uint64_t> &Record, unsigned &Abbrev);
@@ -1898,6 +1902,7 @@ unsigned ModuleBitcodeWriter::createDILocationAbbrev() {
   Abbv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Fixed, 1)); // isImplicitCode
   Abbv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::VBR, 6));   // atomGroup
   Abbv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Fixed, 3)); // atomRank
+  Abbv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::VBR, 6));   // irlayers (0 = none)
   return Stream.EmitAbbrev(std::move(Abbv));
 }
 
@@ -1915,10 +1920,33 @@ void ModuleBitcodeWriter::writeDILocation(const DILocation *N,
   Record.push_back(N->isImplicitCode());
   Record.push_back(N->getAtomGroup());
   Record.push_back(N->getAtomRank());
+  Record.push_back(VE.getMetadataOrNullID(N->getRawIRLayers()));
   Stream.EmitRecord(bitc::METADATA_LOCATION, Record, Abbrev);
   Record.clear();
 }
 
+void ModuleBitcodeWriter::writeDILayerLoc(const DILayerLoc *N,
+                                          SmallVectorImpl<uint64_t> &Record,
+                                          unsigned Abbrev) {
+  Record.push_back(N->isDistinct());
+  Record.push_back(N->getLine());
+  Record.push_back(N->getColumn());
+  Record.push_back(VE.getMetadataID(N->getRawFile()));
+  Record.push_back(VE.getMetadataID(N->getRawKind()));
+  Stream.EmitRecord(bitc::METADATA_LAYERLOC, Record, Abbrev);
+  Record.clear();
+}
+
+void ModuleBitcodeWriter::writeDILayerLocList(const DILayerLocList *N,
+                                              SmallVectorImpl<uint64_t> &Record,
+                                              unsigned Abbrev) {
+  Record.push_back(N->isDistinct());
+  for (auto &I : N->operands())
+    Record.push_back(VE.getMetadataOrNullID(I));
+  Stream.EmitRecord(bitc::METADATA_LAYERLOCLIST, Record, Abbrev);
+  Record.clear();
+}
+
 unsigned ModuleBitcodeWriter::createGenericDINodeAbbrev() {
   // Assume the column is usually under 128, and always output the inlined-at
   // location (it's never more expensive than building an array size 1).
@@ -3873,6 +3901,7 @@ void ModuleBitcodeWriter::writeFunction(
           Vals.push_back(DL->isImplicitCode());
           Vals.push_back(DL->getAtomGroup());
           Vals.push_back(DL->getAtomRank());
+          Vals.push_back(VE.getMetadataOrNullID(DL->getRawIRLayers()));
           Stream.EmitRecord(bitc::FUNC_CODE_DEBUG_LOC, Vals,
                             FUNCTION_DEBUG_LOC_ABBREV);
           Vals.clear();
@@ -4281,6 +4310,7 @@ void ModuleBitcodeWriter::writeBlockInfo() {
     Abbv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Fixed, 1));
     Abbv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::VBR, 6)); // Atom group.
     Abbv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::VBR, 3)); // Atom rank.
+    Abbv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::VBR, 6)); // irlayers (0 = none).
     if (Stream.EmitBlockInfoAbbrev(bitc::FUNCTION_BLOCK_ID, Abbv) !=
         FUNCTION_DEBUG_LOC_ABBREV)
       llvm_unreachable("Unexpected abbrev ordering!");
diff --git a/llvm/lib/CodeGen/MIRParser/MIParser.cpp b/llvm/lib/CodeGen/MIRParser/MIParser.cpp
index bb0b87cc042d0..71be0869ef244 100644
--- a/llvm/lib/CodeGen/MIRParser/MIParser.cpp
+++ b/llvm/lib/CodeGen/MIRParser/MIParser.cpp
@@ -2573,6 +2573,7 @@ bool MIParser::parseDILocation(MDNode *&Loc) {
   bool ImplicitCode = false;
   uint64_t AtomGroup = 0;
   uint64_t AtomRank = 0;
+  MDNode *IRLayers = nullptr;
 
   if (expectAndConsume(MIToken::lparen))
     return true;
@@ -2670,6 +2671,16 @@ bool MIParser::parseDILocation(MDNode *&Loc) {
           lex();
           continue;
         }
+        if (Token.stringValue() == "irlayers") {
+          lex();
+          if (expectAndConsume(MIToken::colon))
+            return true;
+          if (parseMDNode(IRLayers))
+            return error("expected metadata node");
+          if (!isa<DILayerLocList>(IRLayers))
+            return error("expected DILayerLocList node");
+          continue;
+        }
       }
       return error(Twine("invalid DILocation argument '") +
                    Token.stringValue() + "'");
@@ -2685,7 +2696,7 @@ bool MIParser::parseDILocation(MDNode *&Loc) {
     return error("DILocation requires a scope");
 
   Loc = DILocation::get(MF.getFunction().getContext(), Line, Column, Scope,
-                        InlinedAt, ImplicitCode, AtomGroup, AtomRank);
+                        InlinedAt, ImplicitCode, AtomGroup, AtomRank, IRLayers);
   return false;
 }
 
diff --git a/llvm/lib/IR/AsmWriter.cpp b/llvm/lib/IR/AsmWriter.cpp
index cad4f17b0db91..132a08b7a9dc7 100644
--- a/llvm/lib/IR/AsmWriter.cpp
+++ b/llvm/lib/IR/AsmWriter.cpp
@@ -2124,6 +2124,29 @@ static void writeDILocation(raw_ostream &Out, const DILocation *DL,
                     /* Default */ false);
   Printer.printInt("atomGroup", DL->getAtomGroup());
   Printer.printInt<unsigned>("atomRank", DL->getAtomRank());
+  Printer.printMetadata("irlayers", DL->getRawIRLayers());
+  Out << ")";
+}
+
+static void writeDILayerLoc(raw_ostream &Out, const DILayerLoc *N,
+                            AsmWriterContext &WriterCtx) {
+  Out << "!DILayerLoc(";
+  MDFieldPrinter Printer(Out, WriterCtx);
+  Printer.printInt("line", N->getLine(), /* ShouldSkipZero */ false);
+  Printer.printInt("column", N->getColumn());
+  Printer.printMetadata("file", N->getRawFile(), /* ShouldSkipNull */ false);
+  Printer.printString("kind", N->getKind(), /* ShouldSkipEmpty */ false);
+  Out << ")";
+}
+
+static void writeDILayerLocList(raw_ostream &Out, const DILayerLocList *N,
+                                AsmWriterContext &WriterCtx) {
+  Out << "!DILayerLocList(";
+  ListSeparator FS;
+  for (const MDOperand &Op : N->operands()) {
+    Out << FS;
+    writeMetadataAsOperand(Out, Op, WriterCtx);
+  }
   Out << ")";
 }
 
diff --git a/llvm/lib/IR/DebugInfoMetadata.cpp b/llvm/lib/IR/DebugInfoMetadata.cpp
index 5e9e0e4e81c6e..779742f5a02da 100644
--- a/llvm/lib/IR/DebugInfoMetadata.cpp
+++ b/llvm/lib/IR/DebugInfoMetadata.cpp
@@ -13,6 +13,7 @@
 #include "llvm/IR/DebugInfoMetadata.h"
 #include "LLVMContextImpl.h"
 #include "MetadataImpl.h"
+#include "llvm/ADT/DenseSet.h"
 #include "llvm/ADT/SetVector.h"
 #include "llvm/ADT/StringSwitch.h"
 #include "llvm/BinaryFormat/Dwarf.h"
@@ -26,6 +27,7 @@
 
 #include <numeric>
 #include <optional>
+#include <tuple>
 
 using namespace llvm;
 
@@ -60,15 +62,17 @@ DebugVariableAggregate::DebugVariableAggregate(const DbgVariableRecord *DVR)
 
 DILocation::DILocation(LLVMContext &C, StorageType Storage, unsigned Line,
                        unsigned Column, uint64_t AtomGroup, uint8_t AtomRank,
-                       ArrayRef<Metadata *> MDs, bool ImplicitCode)
+                       bool HasIRLayers, ArrayRef<Metadata *> MDs,
+                       bool ImplicitCode)
     : MDNode(C, DILocationKind, Storage, MDs), AtomGroup(AtomGroup),
-      AtomRank(AtomRank) {
+      AtomRank(AtomRank), HasIRLayers(HasIRLayers) {
   assert(AtomRank <= 7 && "AtomRank number should fit in 3 bits");
+  assert(AtomGroup < (1ULL << 60) && "AtomGroup number should fit in 60 bits");
   if (AtomGroup)
     C.updateDILocationAtomGroupWaterline(AtomGroup + 1);
 
-  assert((MDs.size() == 1 || MDs.size() == 2) &&
-         "Expected a scope and optional inlined-at");
+  assert(MDs.size() >= 1 && MDs.size() <= 3 &&
+         "Expected a scope and optional inlined-at + irlayers");
   // Set line and column.
   assert(Column < (1u << 16) && "Expected 16-bit column");
 
@@ -88,15 +92,20 @@ DILocation *DILocation::getImpl(LLVMContext &Context, unsigned Line,
                                 unsigned Column, Metadata *Scope,
                                 Metadata *InlinedAt, bool ImplicitCode,
                                 uint64_t AtomGroup, uint8_t AtomRank,
-                                StorageType Storage, bool ShouldCreate) {
+                                Metadata *IRLayers, StorageType Storage,
+                                bool ShouldCreate) {
   // Fixup column.
   adjustColumn(Column);
 
+  // Clamp rather than truncate, which would wrap into a different valid group.
+  if (AtomGroup >= (1ULL << 60))
+    AtomGroup = 0;
+
   if (Storage == Uniqued) {
-    if (auto *N = getUniqued(Context.pImpl->DILocations,
-                             DILocationInfo::KeyTy(Line, Column, Scope,
-                                                   InlinedAt, ImplicitCode,
-                                                   AtomGroup, AtomRank)))
+    if (auto *N = getUniqued(
+            Context.pImpl->DILocations,
+            DILocationInfo::KeyTy(Line, Column, Scope, InlinedAt, ImplicitCode,
+                                  AtomGroup, AtomRank, IRLayers)))
       return N;
     if (!ShouldCreate)
       return nullptr;
@@ -104,16 +113,62 @@ DILocation *DILocation::getImpl(LLVMContext &Context, unsigned Line,
     assert(ShouldCreate && "Expected non-uniqued nodes to always be created");
   }
 
-  SmallVector<Metadata *, 2> Ops;
+  SmallVector<Metadata *, 3> Ops;
   Ops.push_back(Scope);
   if (InlinedAt)
     Ops.push_back(InlinedAt);
-  return storeImpl(new (Ops.size(), Storage)
-                       DILocation(Context, Storage, Line, Column, AtomGroup,
-                                  AtomRank, Ops, ImplicitCode),
+  if (IRLayers)
+    Ops.push_back(IRLayers);
+  return storeImpl(new (Ops.size(), Storage) DILocation(
+                       Context, Storage, Line, Column, AtomGroup, AtomRank,
+                       /*HasIRLayers=*/IRLayers != nullptr, Ops, ImplicitCode),
                    Storage, Context.pImpl->DILocations);
 }
 
+DILayerLoc *DILayerLoc::getImpl(LLVMContext &Context, MDString *Kind,
+                                Metadata *File, unsigned Line, unsigned Column,
+                                StorageType Storage, bool ShouldCreate) {
+  // Clamp an out-of-range column to 0 (the 16-bit SubclassData limit), as
+  // DILocation::getImpl does; the ctor otherwise asserts and release builds
+  // truncate.
+  adjustColumn(Column);
+  if (Storage == Uniqued) {
+    if (auto *N = getUniqued(Context.pImpl->DILayerLocs,
+                             DILayerLocInfo::KeyTy(Kind, File, Line, Column)))
+      return N;
+    if (!ShouldCreate)
+      return nullptr;
+  }
+  Metadata *Ops[] = {Kind, File};
+  return storeImpl(new (std::size(Ops), Storage)
+                       DILayerLoc(Context, Storage, Line, Column, Ops),
+                   Storage, Context.pImpl->DILayerLocs);
+}
+
+DILayerLocList *DILayerLocList::getImpl(LLVMContext &Context,
+                                        ArrayRef<Metadata *> Layers,
+                                        StorageType Storage,
+                                        bool ShouldCreate) {
+  unsigned Hash = 0;
+  if (Storage == Uniqued) {
+    DILayerLocListInfo::KeyTy Key(Layers);
+    if (auto *N = getUniqued(Context.pImpl->DILayerLocLists, Key))
+      return N;
+    if (!ShouldCreate)
+      return nullptr;
+    Hash = Key.getHash();
+  } else {
+    assert(ShouldCreate && "Expected non-uniqued nodes to always be created");
+  }
+  return storeImpl(new (Layers.size(), Storage)
+                       DILayerLocList(Context, Storage, Hash, Layers),
+                   Storage, Context.pImpl->DILayerLocLists);
+}
+
+void DILayerLocList::recalculateHash() {
+  setHash(DILayerLocListInfo::KeyTy::calculateHash(this));
+}
+
 DILocation *DILocation::getMergedLocations(ArrayRef<DILocation *> Locs) {
   if (Locs.empty())
     return nullptr;
@@ -220,6 +275,40 @@ struct ScopeLocationsMatcher {
   }
 };
 
+// Returns a uniqued DILayerLocList holding the intersection of LocA's and
+// LocB's layer sets, or null if either has no layers or they share none.
+// Entries keep LocA's relative order: LLVM assigns no meaning to layer order,
+// but it is part of a list's identity and visible to consumers, so preserve
+// rather than sort.
+static Metadata *mergeIRLayers(LLVMContext &C, const DILocation *LocA,
+                               const DILocation *LocB) {
+  DILayerLocList *LA = LocA->getIRLayers();
+  DILayerLocList *LB = LocB->getIRLayers();
+  if (!LA || !LB)
+    return nullptr;
+  // Entries match on their fields rather than by pointer: DILayerLocs are
+  // uniqued, but `distinct` ones are legal and must not look disjoint. Keying
+  // on the fields also keeps the intersection linear in the two list lengths.
+  using LayerKey = std::tuple<Metadata *, Metadata *, unsigned, unsigned>;
+  auto keyOf = [](const DILayerLoc *L) {
+    return LayerKey(L->getRawKind(), L->getRawFile(), L->getLine(),
+                    L->getColumn());
+  };
+  SmallDenseSet<LayerKey, 2> BLayers;
+  for (unsigned I = 0, E = LB->getNumLayers(); I != E; ++I)
+    if (const DILayerLoc *R = LB->getLayer(I))
+      BLayers.insert(keyOf(R));
+  SmallVector<Metadata *, 2> Keep;
+  for (unsigned I = 0, E = LA->getNumLayers(); I != E; ++I) {
+    DILayerLoc *L = LA->getLayer(I);
+    if (L && BLayers.contains(keyOf(L)))
+      Keep.push_back(L);
+  }
+  if (Keep.empty())
+    return nullptr;
+  return DILayerLocList::get(C, Keep);
+}
+
 DILocation *DILocation::getMergedLocation(DILocation *LocA, DILocation *LocB) {
   if (LocA == LocB)
     return LocA;
@@ -304,13 +393,19 @@ DILocation *DILocation::getMergedLocation(DILocation *LocA, DILocation *LocB) {
     if (L1 == L2)
       return DILocation::get(C, L1->getLine(), L1->getColumn(), L1->getScope(),
                              InlinedAt, L1->isImplicitCode(),
-                             L1->getAtomGroup(), L1->getAtomRank());
+                             L1->getAtomGroup(), L1->getAtomRank(),
+                             L1->getRawIRLayers());
 
     // If the locations originate from different subprograms we can't produce
     // a common location.
     if (L1->getScope()->getSubprogram() != L2->getScope()->getSubprogram())
       return nullptr;
 
+    // Each merged frame keeps the intersection of the two frames'
+    // intermediate-IR layer sets, so a layer on any frame (e.g. the shared
+    // outermost kernel frame) survives the merge.
+    Metadata *MergedLayers = mergeIRLayers(C, L1, L2);
+
     // Find nearest common scope inside subprogram.
     DIScope *Scope = getNearestMatchingScope<EqualScopesMatcher>(L1, L2).first;
     assert(Scope && "No common scope in the same subprogram?");
@@ -333,7 +428,9 @@ DILocation *DILocation::getMergedLocation(DILocation *LocA, DILocation *LocB) {
       // from CommonLoc. Use it as merged location.
       if (Scope->getFile() != L1->getFile() || L1->getFile() != L2->getFile())
         return DILocation::get(C, CommonLoc.first, CommonLoc.second,
-                               CommonLocScope, InlinedAt);
+                               CommonLocScope, InlinedAt,
+                               /*ImplicitCode=*/false, /*AtomGroup=*/0,
+                               /*AtomRank=*/0, MergedLayers);
     }
 
     bool SameLine = L1->getLine() == L2->getLine();
@@ -346,7 +443,7 @@ DILocation *DILocation::getMergedLocation(DILocation *LocA, DILocation *LocB) {
     // further to do if neither location has an atom number.
     if (!SameLine || !(L1->getAtomGroup() || L2->getAtomGroup()))
       return DILocation::get(C, Line, Col, Scope, InlinedAt, IsImplicitCode,
-                             /*AtomGroup*/ 0, /*AtomRank*/ 0);
+                             /*AtomGroup*/ 0, /*AtomRank*/ 0, MergedLayers);
 
     uint64_t Group = 0;
     uint64_t Rank = 0;
@@ -380,7 +477,7 @@ DILocation *DILocation::getMergedLocation(DILocation *LocA, DILocation *LocB) {
       Rank = 1;
     }
     return DILocation::get(C, Line, Col, Scope, InlinedAt, IsImplicitCode,
-                           Group, Rank);
+                           Group, Rank, MergedLayers);
   };
 
   DILocation *Result = ARIt != ALocs.rend() ? (*ARIt)->getInlinedAt() : nullptr;
diff --git a/llvm/lib/IR/DebugLoc.cpp b/llvm/lib/IR/DebugLoc.cpp
index 16a82c2a05208..87bbb91acb97f 100644
--- a/llvm/lib/IR/DebugLoc.cpp
+++ b/llvm/lib/IR/DebugLoc.cpp
@@ -60,6 +60,11 @@ DILocation *DebugLoc::getInlinedAt() const {
   return get()->getInlinedAt();
 }
 
+MDNode *DebugLoc::getRawIRLayers() const {
+  DILocation *L = get();
+  return L ? cast_if_present<MDNode>(L->getRawIRLayers()) : nullptr;
+}
+
 MDNode *DebugLoc::getInlinedAtScope() const {
   return cast<DILocation>(Loc)->getInlinedAtScope();
 }
@@ -149,9 +154,14 @@ DebugLoc DebugLoc::appendInlinedAt(const DebugLoc &DL, DILocation *InlinedAt,
   // location (then rebuilding the rest of the chain behind it) and update the
   // map of already-constructed inlined-at nodes.
   // Key Instructions: InlinedAt fields don't need atom info.
+  // Preserve each frame's irlayers -- the intermediate-IR snapshot can
+  // live on any frame (the one outermost at snapshot time), so the chain
+  // rebuild must not drop it.
   for (const DILocation *MD : reverse(InlinedAtLocations))
     Cache[MD] = Last = DILocation::getDistinct(
-        Ctx, MD->getLine(), MD->getColumn(), MD->getScope(), Last);
+        Ctx, MD->getLine(), MD->getColumn(), MD->getScope(), Last,
+        /*ImplicitCode=*/false, /*AtomGroup=*/0, /*AtomRank=*/0,
+        MD->getRawIRLayers());
 
   return Last;
 }
diff --git a/llvm/lib/IR/LLVMContextImpl.h b/llvm/lib/IR/LLVMContextImpl.h
index 41c8a92c56eda..b7736dd84a714 100644
--- a/llvm/lib/IR/LLVMContextImpl.h
+++ b/llvm/lib/IR/LLVMContextImpl.h
@@ -277,7 +277,8 @@ template <> struct MDNodeKeyImpl<MDTuple> : MDNodeOpsKey {
 template <> struct MDNodeKeyImpl<DILocation> {
   Metadata *Scope;
   Metadata *InlinedAt;
-  uint64_t AtomGroup : 61;
+  Metadata *IRLayers;
+  uint64_t AtomGroup : 60;
   uint64_t AtomRank : 3;
   unsigned Line;
   uint16_t Column;
@@ -285,22 +286,23 @@ template <> struct MDNodeKeyImpl<DILocation> {
 
   MDNodeKeyImpl(unsigned Line, uint16_t Column, Metadata *Scope,
                 Metadata *InlinedAt, bool ImplicitCode, uint64_t AtomGroup,
-                uint8_t AtomRank)
-      : Scope(Scope), InlinedAt(InlinedAt), AtomGroup(AtomGroup),
-        AtomRank(AtomRank), Line(Line), Column(Column),
+                uint8_t AtomRank, Metadata *IRLayers)
+      : Scope(Scope), InlinedAt(InlinedAt), IRLayers(IRLayers),
+        AtomGroup(AtomGroup), AtomRank(AtomRank), Line(Line), Column(Column),
         ImplicitCode(ImplicitCode) {}
 
   MDNodeKeyImpl(const DILocation *L)
       : Scope(L->getRawScope()), InlinedAt(L->getRawInlinedAt()),
-        AtomGroup(L->getAtomGroup()), AtomRank(L->getAtomRank()),
-        Line(L->getLine()), Column(L->getColumn()),
+        IRLayers(L->getRawIRLayers()), AtomGroup(L->getAtomGroup()),
+        AtomRank(L->getAtomRank()), Line(L->getLine()), Column(L->getColumn()),
         ImplicitCode(L->isImplicitCode()) {}
 
   bool isKeyOf(const DILocation *RHS) const {
     return Line == RHS->getLine() && Column == RHS->getColumn() &&
            Scope == RHS->getRawScope() && InlinedAt == RHS->getRawInlinedAt() &&
            ImplicitCode == RHS->isImplicitCode() &&
-           AtomGroup == RHS->getAtomGroup() && AtomRank == RHS->getAtomRank();
+           AtomGroup == RHS->getAtomGroup() && AtomRank == RHS->getAtomRank() &&
+           IRLayers == RHS->getRawIRLayers();
   }
 
   unsigned getHashValue() const {
@@ -313,13 +315,59 @@ template <> struct MDNodeKeyImpl<DILocation> {
     // messing with the hash distribution* appear to still be massively
     // outweighed by the overall compile time savings by performing this check.
     // * (hash_combine(x) != hash_combine(x, 0))
-    if (AtomGroup || AtomRank)
+    // irlayers is likewise rare, so it is only mixed in when present to keep
+    // the common no-layers hashes unchanged.
+    if (AtomGroup || AtomRank) {
+      if (IRLayers)
+        return hash_combine(LineColumnAndImplicitCode, Scope, InlinedAt,
+                            AtomGroup | (uint64_t(AtomRank) << 61), IRLayers);
       return hash_combine(LineColumnAndImplicitCode, Scope, InlinedAt,
                           AtomGroup | (uint64_t(AtomRank) << 61));
+    }
+    if (IRLayers)
+      return hash_combine(LineColumnAndImplicitCode, Scope, InlinedAt,
+                          IRLayers);
     return hash_combine(LineColumnAndImplicitCode, Scope, InlinedAt);
   }
 };
 
+/// DenseMapInfo for DILayerLoc.
+template <> struct MDNodeKeyImpl<DILayerLoc> {
+  Metadata *Kind;
+  Metadata *File;
+  unsigned Line;
+  uint16_t Column;
+
+  MDNodeKeyImpl(Metadata *Kind, Metadata *File, unsigned Line, uint16_t Column)
+      : Kind(Kind), File(File), Line(Line), Column(Column) {}
+  MDNodeKeyImpl(const DILayerLoc *N)
+      : Kind(N->getRawKind()), File(N->getRawFile()), Line(N->getLine()),
+        Column(N->getColumn()) {}
+
+  bool isKeyOf(const DILayerLoc *RHS) const {
+    return Kind == RHS->getRawKind() && File == RHS->getRawFile() &&
+           Line == RHS->getLine() && Column == RHS->getColumn();
+  }
+
+  unsigned getHashValue() const {
+    return hash_combine(Kind, File, Line, Column);
+  }
+};
+
+/// DenseMapInfo for DILayerLocList.
+template <> struct MDNodeKeyImpl<DILayerLocList> : MDNodeOpsKey {
+  MDNodeKeyImpl(ArrayRef<Metadata *> Ops) : MDNodeOpsKey(Ops) {}
+  MDNodeKeyImpl(const DILayerLocList *N) : MDNodeOpsKey(N) {}
+
+  bool isKeyOf(const DILayerLocList *RHS) const { return compareOps(RHS); }
+
+  unsigned getHashValue() const { return getHash(); }
+
+  static unsigned calculateHash(DILayerLocList *N) {
+    return MDNodeOpsKey::calculateHash(N);
+  }
+};
+
 /// DenseMapInfo for GenericDINode.
 template <> struct MDNodeKeyImpl<GenericDINode> : MDNodeOpsKey {
   unsigned Tag;
diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index 09429024e3ae8..d1957081fd444 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -1089,6 +1089,22 @@ void Verifier::visitDILocation(const DILocation &N) {
     CheckDI(isa<DILocation>(IA), "inlined-at should be a location", &N, IA);
   if (auto *SP = dyn_cast<DISubprogram>(N.getRawScope()))
     CheckDI(SP->isDefinition(), "scope points into the type hierarchy", &N);
+  if (auto *L = N.getRawIRLayers())
+    CheckDI(isa<DILayerLocList>(L), "irlayers must be a DILayerLocList", &N, L);
+}
+
+void Verifier::visitDILayerLoc(const DILayerLoc &N) {
+  CheckDI(isa_and_nonnull<MDString>(N.getRawKind()),
+          "layer kind must be a non-null MDString", &N, N.getRawKind());
+  CheckDI(isa_and_nonnull<DIFile>(N.getRawFile()),
+          "layer file must be a non-null DIFile", &N, N.getRawFile());
+}
+
+void Verifier::visitDILayerLocList(const DILayerLocList &N) {
+  CheckDI(N.getNumLayers() > 0, "DILayerLocList must be non-empty", &N);
+  for (const MDOperand &Op : N.layers())
+    CheckDI(isa_and_nonnull<DILayerLoc>(Op.get()),
+            "DILayerLocList entry must be a DILayerLoc", &N, Op.get());
 }
 
 void Verifier::visitGenericDINode(const GenericDINode &N) {
diff --git a/llvm/lib/Target/DirectX/DXILWriter/DXILBitcodeWriter.cpp b/llvm/lib/Target/DirectX/DXILWriter/DXILBitcodeWriter.cpp
index 6f14f70014b01..6056414a61018 100644
--- a/llvm/lib/Target/DirectX/DXILWriter/DXILBitcodeWriter.cpp
+++ b/llvm/lib/Target/DirectX/DXILWriter/DXILBitcodeWriter.cpp
@@ -299,6 +299,14 @@ class DXILBitcodeWriter {
     //   See https://github.com/llvm/llvm-project/issues/58989
     llvm_unreachable("DXIL cannot contain DIAssignID Nodes");
   }
+  void writeDILayerLoc(const DILayerLoc *N, SmallVectorImpl<uint64_t> &Record,
+                       unsigned Abbrev) {
+    llvm_unreachable("DXIL cannot contain DILayerLoc Nodes");
+  }
+  void writeDILayerLocList(const DILayerLocList *N,
+                           SmallVectorImpl<uint64_t> &Record, unsigned Abbrev) {
+    llvm_unreachable("DXIL cannot contain DILayerLocList Nodes");
+  }
   void writeDIModule(const DIModule *N, SmallVectorImpl<uint64_t> &Record,
                      unsigned Abbrev);
   void writeDITemplateTypeParameter(const DITemplateTypeParameter *N,
diff --git a/llvm/test/Bitcode/intermediate-loc-metadata-location.ll b/llvm/test/Bitcode/intermediate-loc-metadata-location.ll
new file mode 100644
index 0000000000000..89d16e12e5875
--- /dev/null
+++ b/llvm/test/Bitcode/intermediate-loc-metadata-location.ll
@@ -0,0 +1,58 @@
+; RUN: llvm-as < %s | llvm-dis | FileCheck %s
+;;
+;; Checks that irlayers survive a bitcode round-trip on a DILocation written as
+;; a METADATA_LOCATION record -- one reachable only as an inlinedAt target, with
+;; a layer list built through forward references.
+
+define dso_local void @test_kernel(ptr noundef %v) #0 !dbg !8 {
+entry:
+  store ptr %v, ptr %v, align 8, !dbg !20
+  store ptr %v, ptr %v, align 8, !dbg !23
+  ret void, !dbg !21
+}
+
+attributes #0 = { noinline optnone }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cu", directory: "/test")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!8 = distinct !DISubprogram(name: "test_kernel", scope: !1, file: !1, line: 1, type: !9, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null}
+!11 = distinct !DISubprogram(name: "helper", scope: !1, file: !1, line: 20, type: !9, scopeLine: 20, spFlags: DISPFlagDefinition, unit: !0)
+
+!14 = !DIFile(filename: "kernel.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", source: "tile ir text")
+!15 = !DIFile(filename: "kernel.gpuir", directory: ".", checksumkind: CSK_MD5, checksum: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", source: "gpu ir text")
+
+;; !18 precedes its entries, so they parse as forward references and the list is
+;; re-uniqued once they resolve. !22 names the same entries after they resolve,
+;; so it must land on !18's node -- a stale cached hash would miss the uniquing
+;; lookup and produce a second, identical list.
+!18 = !DILayerLocList(!16, !17)
+!16 = !DILayerLoc(line: 100, column: 1, file: !14, kind: "tile ir")
+!17 = !DILayerLoc(line: 7, column: 3, file: !15, kind: "gpu ir")
+!22 = !DILayerLocList(!16, !17)
+
+!19 = distinct !DILocation(line: 30, column: 1, scope: !8, irlayers: !18)
+!24 = distinct !DILocation(line: 40, column: 1, scope: !8, irlayers: !22)
+!20 = !DILocation(line: 21, column: 5, scope: !11, inlinedAt: !19)
+!21 = !DILocation(line: 22, column: 1, scope: !11, inlinedAt: !19)
+!23 = !DILocation(line: 23, column: 5, scope: !11, inlinedAt: !24)
+
+;; The metadata-block location keeps its layers, and the two entries keep their
+;; order.
+; CHECK-DAG: ![[IA:[0-9]+]] = distinct !DILocation(line: 30, column: 1, scope: !{{[0-9]+}}, irlayers: ![[LIST:[0-9]+]])
+; CHECK-DAG: ![[LIST]] = !DILayerLocList(![[TILE:[0-9]+]], ![[GPU:[0-9]+]])
+; CHECK-DAG: ![[TILE]] = !DILayerLoc(line: 100, column: 1, file: !{{[0-9]+}}, kind: "tile ir")
+; CHECK-DAG: ![[GPU]] = !DILayerLoc(line: 7, column: 3, file: !{{[0-9]+}}, kind: "gpu ir")
+
+;; Both layered locations reference that one list node.
+; CHECK-DAG: = distinct !DILocation(line: 40, column: 1, scope: !{{[0-9]+}}, irlayers: ![[LIST]])
+
+;; The instruction locations reference them as inlinedAt and carry no layers of
+;; their own.
+; CHECK-DAG: !DILocation(line: 21, column: 5, scope: !{{[0-9]+}}, inlinedAt: ![[IA]])
diff --git a/llvm/test/Bitcode/intermediate-loc-roundtrip.ll b/llvm/test/Bitcode/intermediate-loc-roundtrip.ll
new file mode 100644
index 0000000000000..89e060e3efbf1
--- /dev/null
+++ b/llvm/test/Bitcode/intermediate-loc-roundtrip.ll
@@ -0,0 +1,57 @@
+; RUN: llvm-as < %s | llvm-dis | FileCheck %s
+; RUN: verify-uselistorder %s
+
+;; Test that intermediate location metadata (used for multi-level line info)
+;; round-trips correctly through bitcode. The layers live on the
+;; DILocation's typed `irlayers` operand (a DILayerLocList of DILayerLoc
+;; entries), so the whole DILocation/DILayerLocList/DILayerLoc/DIFile chain must
+;; survive .ll -> .bc -> .ll.
+
+define dso_local void @test_kernel(ptr noundef %v) !dbg !8 {
+entry:
+  %v.addr = alloca ptr, align 8
+  store ptr %v, ptr %v.addr, align 8, !dbg !20
+  %0 = load ptr, ptr %v.addr, align 8, !dbg !21
+  ret void, !dbg !22
+}
+
+; CHECK-LABEL: define dso_local void @test_kernel
+; CHECK: store ptr %v, ptr %v.addr, align 8, !dbg ![[DBG1:[0-9]+]]
+; CHECK: load ptr, ptr %v.addr, align 8, !dbg ![[DBG2:[0-9]+]]
+; CHECK: ret void, !dbg ![[DBG3:[0-9]+]]
+
+;; Verify the metadata structure is preserved: each !dbg is a DILocation whose
+;; source line/col comes from the primary location and whose `irlayers` operand
+;; points at a shared (uniqued) DILayerLocList.
+
+; CHECK-DAG: ![[DBG1]] = !DILocation(line: 2, column: 5, scope: ![[SP:[0-9]+]], irlayers: ![[LIST:[0-9]+]])
+; CHECK-DAG: ![[DBG2]] = !DILocation(line: 3, column: 5, scope: ![[SP]], irlayers: ![[LIST]])
+; CHECK-DAG: ![[DBG3]] = !DILocation(line: 4, column: 1, scope: ![[SP]], irlayers: ![[LIST]])
+
+;; The shared layer list holds one DILayerLoc with the kind string and the
+;; intermediate coordinate.
+; CHECK-DAG: ![[LIST]] = !DILayerLocList(![[LAYER:[0-9]+]])
+; CHECK-DAG: ![[LAYER]] = !DILayerLoc(line: 100, column: 10, file: ![[INTFILE:[0-9]+]], kind: "TileIR")
+; CHECK-DAG: ![[INTFILE]] = !DIFile(filename: "intermediate.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "ffffffffffffffffffffffffffffffff")
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cu", directory: "/test")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!8 = distinct !DISubprogram(name: "test_kernel", scope: !1, file: !1, line: 1, type: !9, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null}
+
+;; Intermediate-IR layer: one DILayerLoc, shared (uniqued) across all layered
+;; instructions via a single DILayerLocList.
+!14 = !DIFile(filename: "intermediate.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "ffffffffffffffffffffffffffffffff")
+!30 = !DILayerLocList(!31)
+!31 = !DILayerLoc(line: 100, column: 10, file: !14, kind: "TileIR")
+
+;; Layered instruction locations: primary source loc + shared irlayers.
+!20 = !DILocation(line: 2, column: 5, scope: !8, irlayers: !30)
+!21 = !DILocation(line: 3, column: 5, scope: !8, irlayers: !30)
+!22 = !DILocation(line: 4, column: 1, scope: !8, irlayers: !30)
diff --git a/llvm/test/CodeGen/MIR/X86/instructions-debug-location-irlayers.mir b/llvm/test/CodeGen/MIR/X86/instructions-debug-location-irlayers.mir
new file mode 100644
index 0000000000000..093e9051601b7
--- /dev/null
+++ b/llvm/test/CodeGen/MIR/X86/instructions-debug-location-irlayers.mir
@@ -0,0 +1,64 @@
+# RUN: llc -mtriple=x86_64 -run-pass none -o - %s | FileCheck %s
+# RUN: llc -mtriple=x86_64 -run-pass none -o - %s \
+# RUN:   | llc -x mir -mtriple=x86_64 -run-pass none -o - | FileCheck %s
+## Check that the MIR parser accepts `irlayers` on a DILocation and that the
+## printer emits it again. The second RUN line feeds the printed MIR back through
+## llc, so a layer list that prints as something unparsable fails rather than
+## passing quietly. The first location carries irlayers without an inlinedAt, the
+## second carries both.
+
+# CHECK-DAG: ![[LIST:[0-9]+]] = !DILayerLocList(![[LAYER:[0-9]+]])
+# CHECK-DAG: ![[LAYER]] = !DILayerLoc(line: 42, column: 5, file: !{{[0-9]+}}, kind: "tile ir")
+# CHECK-LABEL: name: test
+# CHECK: $eax = COPY %0, debug-location !DILocation(line: 1, scope: !{{[0-9]+}}, irlayers: ![[LIST]])
+# CHECK: RET64 $eax, debug-location !DILocation(line: 2, scope: !{{[0-9]+}}, inlinedAt: !DILocation(line: 1, scope: !{{[0-9]+}}), irlayers: ![[LIST]])
+
+--- |
+
+  define i32 @test(i32 %x) #0 !dbg !4 {
+  entry:
+    %x.addr = alloca i32, align 4
+    store i32 %x, ptr %x.addr, align 4
+    %0 = load i32, ptr %x.addr, align 4, !dbg !15
+    ret i32 %0, !dbg !14
+  }
+
+  attributes #0 = { nounwind "frame-pointer"="none" }
+
+  !llvm.dbg.cu = !{!0}
+  !llvm.module.flags = !{!9, !10}
+
+  !0 = distinct !DICompileUnit(language: DW_LANG_C99, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, enums: !2, retainedTypes: !2, globals: !2, imports: !2)
+  !1 = !DIFile(filename: "test.ll", directory: "")
+  !2 = !{}
+  !4 = distinct !DISubprogram(name: "test", scope: !5, file: !5, line: 4, type: !6, isDefinition: true, scopeLine: 4, flags: DIFlagPrototyped, isOptimized: false, unit: !0, retainedNodes: !2)
+  !5 = !DIFile(filename: "test.c", directory: "")
+  !6 = !DISubroutineType(types: !7)
+  !7 = !{!8, !8}
+  !8 = !DIBasicType(name: "int", size: 32, align: 32, encoding: DW_ATE_signed)
+  !9 = !{i32 2, !"Dwarf Version", i32 4}
+  !10 = !{i32 2, !"Debug Info Version", i32 3}
+  !14 = !DILocation(line: 8, scope: !4)
+  !15 = !DILocation(line: 9, scope: !4, irlayers: !22)
+  !20 = !DIFile(filename: "kernel.tileir", directory: ".")
+  !21 = !DILayerLoc(line: 42, column: 5, file: !20, kind: "tile ir")
+  !22 = !DILayerLocList(!21)
+
+...
+---
+name:            test
+tracksRegLiveness: true
+registers:
+  - { id: 0, class: gr32 }
+frameInfo:
+  maxAlignment:  4
+stack:
+  - { id: 0, name: x.addr, size: 4, alignment: 4 }
+body: |
+  bb.0.entry:
+    liveins: $edi
+
+    %0 = COPY $edi
+    $eax = COPY %0, debug-location !DILocation(line: 1, scope: !4, irlayers: !22)
+    RET64 $eax, debug-location !DILocation(line: 2, scope: !4, inlinedAt: !DILocation(line: 1, scope: !4), irlayers: !22)
+...
diff --git a/llvm/test/Verifier/intermediate-layerloc.ll b/llvm/test/Verifier/intermediate-layerloc.ll
new file mode 100644
index 0000000000000..c5409ffc168a7
--- /dev/null
+++ b/llvm/test/Verifier/intermediate-layerloc.ll
@@ -0,0 +1,37 @@
+; RUN: llvm-as -disable-output %s -o - 2>&1 | FileCheck --allow-empty %s
+;;
+;; Verification of DILayerLoc is structural only, so a checksum-less intermediate
+;; file verifies clean -- with or without source.
+
+; CHECK-NOT: requires a checksum
+; CHECK-NOT: ignoring invalid debug info
+
+define void @k(ptr %p) !dbg !5 {
+  store ptr null, ptr %p, align 8, !dbg !20
+  store ptr null, ptr %p, align 8, !dbg !21
+  ret void
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cu", directory: "/test")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !DISubroutineType(types: !{null})
+!5 = distinct !DISubprogram(name: "k", scope: !1, file: !1, line: 1, type: !4, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+
+;; No checksum, no source.
+!10 = !DIFile(filename: "kernel.tileir", directory: ".")
+!11 = !DILayerLoc(line: 42, column: 5, file: !10, kind: "tile ir")
+!12 = !DILayerLocList(!11)
+
+;; Source but still no checksum -- the case NVPTX would otherwise use a digest
+;; for, and the one that used to be rejected outright.
+!13 = !DIFile(filename: "kernel.gpuir", directory: ".", source: "gpu ir source")
+!14 = !DILayerLoc(line: 7, column: 1, file: !13, kind: "gpu ir")
+!15 = !DILayerLocList(!14)
+
+!20 = !DILocation(line: 2, column: 5, scope: !5, irlayers: !12)
+!21 = !DILocation(line: 3, column: 5, scope: !5, irlayers: !15)
diff --git a/llvm/unittests/IR/DebugInfoTest.cpp b/llvm/unittests/IR/DebugInfoTest.cpp
index 4004ab4b080b2..7e23d38f9ad71 100644
--- a/llvm/unittests/IR/DebugInfoTest.cpp
+++ b/llvm/unittests/IR/DebugInfoTest.cpp
@@ -12,6 +12,7 @@
 #include "llvm/AsmParser/Parser.h"
 #include "llvm/IR/DIBuilder.h"
 #include "llvm/IR/DebugInfoMetadata.h"
+#include "llvm/IR/DebugLoc.h"
 #include "llvm/IR/DebugProgramInstruction.h"
 #include "llvm/IR/IRBuilder.h"
 #include "llvm/IR/IntrinsicInst.h"
@@ -1511,4 +1512,405 @@ TEST(DIBuilder, DynamicOffsetAndSize) {
   EXPECT_EQ(Field->getRawSizeInBits(), Len);
 }
 
+// Tests for DebugLoc with intermediate location support.
+
+TEST(DebugLocTest, IntermediateLocBasics) {
+  LLVMContext Ctx;
+  auto M = std::make_unique<Module>("MyModule", Ctx);
+  DIBuilder DIB(*M);
+  DIFile *F = DIB.createFile("source.cu", "/");
+  DIFile *IntF = DIB.createFile("intermediate.mlir", "/");
+  DICompileUnit *CU =
+      DIB.createCompileUnit(dwarf::DW_LANG_C, F, "test", false, "", 0);
+  DISubprogram *SP =
+      DIB.createFunction(CU, "foo", "", F, 1, DIB.createSubroutineType({}), 1,
+                         DINode::FlagZero, DISubprogram::SPFlagDefinition);
+
+  // Build a layered DILocation: a source coordinate plus one TileIR layer that
+  // hangs off the DILocation's typed `irlayers` operand.
+  MDString *Kind = MDString::get(Ctx, "TileIR");
+  DILayerLoc *Layer = DILayerLoc::get(Ctx, Kind, IntF, 100, 1);
+  DILayerLocList *Layers = DILayerLocList::get(Ctx, {Layer});
+  DILocation *Loc =
+      DILocation::get(Ctx, 10, 5, SP, /*InlinedAt=*/nullptr,
+                      /*ImplicitCode=*/false, /*AtomGroup=*/0, /*AtomRank=*/0,
+                      /*IRLayers=*/Layers);
+
+  DebugLoc DL(Loc);
+  EXPECT_TRUE((bool)DL);
+  EXPECT_EQ(DL.get(), Loc);
+  EXPECT_EQ(DL.getLine(), 10u);
+  EXPECT_EQ(DL.getCol(), 5u);
+
+  // The layer list is reachable through both DebugLoc and DILocation.
+  EXPECT_EQ(DL.getRawIRLayers(), Layers);
+  ASSERT_EQ(Loc->getIRLayers(), Layers);
+  ASSERT_EQ(Loc->getNumLayers(), 1u);
+  DILayerLoc *L0 = Loc->getLayer(0);
+  ASSERT_NE(L0, nullptr);
+  EXPECT_EQ(L0->getKind(), "TileIR");
+  EXPECT_EQ(L0->getFile(), IntF);
+  EXPECT_EQ(L0->getLine(), 100u);
+  EXPECT_EQ(L0->getColumn(), 1u);
+}
+
+TEST(DebugLocTest, IntermediateLocWithAndWithout) {
+  LLVMContext Ctx;
+  auto M = std::make_unique<Module>("MyModule", Ctx);
+  DIBuilder DIB(*M);
+  DIFile *F = DIB.createFile("source.cu", "/");
+  DIFile *IntF = DIB.createFile("intermediate.mlir", "/");
+  DICompileUnit *CU =
+      DIB.createCompileUnit(dwarf::DW_LANG_C, F, "test", false, "", 0);
+  DISubprogram *SP =
+      DIB.createFunction(CU, "foo", "", F, 1, DIB.createSubroutineType({}), 1,
+                         DINode::FlagZero, DISubprogram::SPFlagDefinition);
+
+  // A source-only DILocation has no layers.
+  DILocation *SourceLoc = DILocation::get(Ctx, 10, 5, SP);
+  DebugLoc DLSourceOnly(SourceLoc);
+  EXPECT_EQ(SourceLoc->getRawIRLayers(), nullptr);
+  EXPECT_EQ(SourceLoc->getIRLayers(), nullptr);
+  EXPECT_EQ(SourceLoc->getNumLayers(), 0u);
+  EXPECT_EQ(DLSourceOnly.getRawIRLayers(), nullptr);
+  EXPECT_EQ(DLSourceOnly.get(), SourceLoc);
+
+  // A layered DILocation returns its list.
+  MDString *Kind = MDString::get(Ctx, "TileIR");
+  DILayerLoc *Layer = DILayerLoc::get(Ctx, Kind, IntF, 100, 1);
+  DILayerLocList *Layers = DILayerLocList::get(Ctx, {Layer});
+  DILocation *LayeredLoc =
+      DILocation::get(Ctx, 10, 5, SP, /*InlinedAt=*/nullptr,
+                      /*ImplicitCode=*/false,
+                      /*AtomGroup=*/0, /*AtomRank=*/0,
+                      /*IRLayers=*/Layers);
+  DebugLoc DLWithInt(LayeredLoc);
+  EXPECT_EQ(DLWithInt.getRawIRLayers(), Layers);
+  EXPECT_EQ(LayeredLoc->getIRLayers(), Layers);
+  ASSERT_EQ(LayeredLoc->getNumLayers(), 1u);
+  EXPECT_EQ(LayeredLoc->getLayer(0)->getKind(), "TileIR");
+}
+
+TEST(DebugLocTest, IntermediateLocEquality) {
+  LLVMContext Ctx;
+  auto M = std::make_unique<Module>("MyModule", Ctx);
+  DIBuilder DIB(*M);
+  DIFile *F = DIB.createFile("source.cu", "/");
+  DIFile *IntF = DIB.createFile("intermediate.mlir", "/");
+  DICompileUnit *CU =
+      DIB.createCompileUnit(dwarf::DW_LANG_C, F, "test", false, "", 0);
+  DISubprogram *SP =
+      DIB.createFunction(CU, "foo", "", F, 1, DIB.createSubroutineType({}), 1,
+                         DINode::FlagZero, DISubprogram::SPFlagDefinition);
+
+  MDString *Kind = MDString::get(Ctx, "TileIR");
+  DILayerLoc *LayerA = DILayerLoc::get(Ctx, Kind, IntF, 100, 1);
+  // Differs from LayerA in a single field (column only) -- a minimal structural
+  // change must still uniquify to a distinct node, so DL1 and DL3 differ.
+  DILayerLoc *LayerB = DILayerLoc::get(Ctx, Kind, IntF, 100, 2);
+  DILayerLocList *ListA = DILayerLocList::get(Ctx, {LayerA});
+  // A structurally-identical list uniques to the same node.
+  DILayerLocList *ListA2 = DILayerLocList::get(Ctx, {LayerA});
+  DILayerLocList *ListB = DILayerLocList::get(Ctx, {LayerB});
+  EXPECT_EQ(ListA, ListA2);
+  EXPECT_NE(ListA, ListB);
+
+  auto Layered = [&](DILayerLocList *L) {
+    return DILocation::get(Ctx, 10, 5, SP, /*InlinedAt=*/nullptr,
+                           /*ImplicitCode=*/false, /*AtomGroup=*/0,
+                           /*AtomRank=*/0, /*IRLayers=*/L);
+  };
+  DebugLoc DL1(Layered(ListA));
+  // A *distinct* location with the same fields and the same (uniqued) layer
+  // list, so isSameSourceLocation cannot short-circuit on pointer identity and
+  // must actually run the structural getRawIRLayers() comparison.
+  DebugLoc DL2(DILocation::getDistinct(Ctx, 10, 5, SP, /*InlinedAt=*/nullptr,
+                                       /*ImplicitCode=*/false, /*AtomGroup=*/0,
+                                       /*AtomRank=*/0, /*IRLayers=*/ListA));
+  DebugLoc DL3(Layered(ListB));
+  DebugLoc DL4(DILocation::get(Ctx, 10, 5, SP)); // no layers
+
+  ASSERT_NE(DL1.get(), DL2.get());
+  // Distinct nodes with same line/col/scope and identical (uniqued) layers ->
+  // same source location (exercises the layer-aware structural compare).
+  EXPECT_TRUE(DL1.isSameSourceLocation(DL2));
+  // Same line/col/scope but different layers -> NOT the same source location.
+  EXPECT_FALSE(DL1.isSameSourceLocation(DL3));
+  // One has layers, the other doesn't -> NOT the same source location.
+  EXPECT_FALSE(DL1.isSameSourceLocation(DL4));
+}
+
+TEST(DebugLocTest, MergedLocationWithIntermediate) {
+  LLVMContext Ctx;
+  auto M = std::make_unique<Module>("MyModule", Ctx);
+  DIBuilder DIB(*M);
+  DIFile *F = DIB.createFile("source.cu", "/");
+  DIFile *IntF = DIB.createFile("intermediate.mlir", "/");
+  DICompileUnit *CU =
+      DIB.createCompileUnit(dwarf::DW_LANG_C, F, "test", false, "", 0);
+  DISubprogram *SP =
+      DIB.createFunction(CU, "foo", "", F, 1, DIB.createSubroutineType({}), 1,
+                         DINode::FlagZero, DISubprogram::SPFlagDefinition);
+
+  MDString *Kind = MDString::get(Ctx, "TileIR");
+  // A layer shared by both locations, plus a distinct layer on each so the two
+  // DILocations are different nodes.
+  DILayerLoc *Shared = DILayerLoc::get(Ctx, Kind, IntF, 100, 1);
+  DILayerLoc *ExtraA = DILayerLoc::get(Ctx, Kind, IntF, 111, 1);
+  DILayerLoc *ExtraB = DILayerLoc::get(Ctx, Kind, IntF, 222, 1);
+  DILayerLocList *List1 = DILayerLocList::get(Ctx, {Shared, ExtraA});
+  DILayerLocList *List2 = DILayerLocList::get(Ctx, {Shared, ExtraB});
+
+  auto Layered = [&](DILayerLocList *L) {
+    return DILocation::get(Ctx, 10, 5, SP, /*InlinedAt=*/nullptr,
+                           /*ImplicitCode=*/false, /*AtomGroup=*/0,
+                           /*AtomRank=*/0, /*IRLayers=*/L);
+  };
+  DILocation *Loc1 = Layered(List1);
+  DILocation *Loc2 = Layered(List2);
+  ASSERT_NE(Loc1, Loc2);
+
+  // Merging two locations that share a DILayerLoc keeps the shared layer only.
+  DILocation *Merged = DILocation::getMergedLocation(Loc1, Loc2);
+  ASSERT_NE(Merged, nullptr);
+  EXPECT_EQ(Merged->getLine(), 10u);
+  ASSERT_NE(Merged->getIRLayers(), nullptr);
+  ASSERT_EQ(Merged->getNumLayers(), 1u);
+  EXPECT_EQ(Merged->getLayer(0), Shared);
+}
+
+TEST(DebugLocTest, MergedLocationPartialIntermediate) {
+  LLVMContext Ctx;
+  auto M = std::make_unique<Module>("MyModule", Ctx);
+  DIBuilder DIB(*M);
+  DIFile *F = DIB.createFile("source.cu", "/");
+  DIFile *IntF = DIB.createFile("intermediate.mlir", "/");
+  DICompileUnit *CU =
+      DIB.createCompileUnit(dwarf::DW_LANG_C, F, "test", false, "", 0);
+  DISubprogram *SP =
+      DIB.createFunction(CU, "foo", "", F, 1, DIB.createSubroutineType({}), 1,
+                         DINode::FlagZero, DISubprogram::SPFlagDefinition);
+
+  MDString *Kind = MDString::get(Ctx, "TileIR");
+  DILayerLoc *LayerA = DILayerLoc::get(Ctx, Kind, IntF, 100, 1);
+  DILayerLocList *LayersA = DILayerLocList::get(Ctx, {LayerA});
+  DILayerLoc *LayerB = DILayerLoc::get(Ctx, Kind, IntF, 200, 2);
+  DILayerLocList *LayersB = DILayerLocList::get(Ctx, {LayerB});
+
+  DILocation *Loc1 =
+      DILocation::get(Ctx, 10, 5, SP, /*InlinedAt=*/nullptr,
+                      /*ImplicitCode=*/false, /*AtomGroup=*/0, /*AtomRank=*/0,
+                      /*IRLayers=*/LayersA);
+  DILocation *Loc2 = DILocation::get(Ctx, 10, 5, SP); // no layers
+  DILocation *Loc3 =
+      DILocation::get(Ctx, 10, 5, SP, /*InlinedAt=*/nullptr,
+                      /*ImplicitCode=*/false, /*AtomGroup=*/0, /*AtomRank=*/0,
+                      /*IRLayers=*/LayersB); // disjoint layer set
+
+  // One side has no layers at all -> merged keeps none (the !LA || !LB
+  // early-out in mergeIRLayers).
+  DILocation *M12 = DILocation::getMergedLocation(Loc1, Loc2);
+  ASSERT_NE(M12, nullptr);
+  EXPECT_EQ(M12->getLine(), 10u);
+  EXPECT_EQ(M12->getRawIRLayers(), nullptr);
+
+  // Both sides have non-empty but DISJOINT layer sets -> empty intersection ->
+  // merged keeps no layers (exercises mergeIRLayers' Keep.empty() path, which
+  // must not attach an invalid empty DILayerLocList).
+  DILocation *M13 = DILocation::getMergedLocation(Loc1, Loc3);
+  ASSERT_NE(M13, nullptr);
+  EXPECT_EQ(M13->getRawIRLayers(), nullptr);
+}
+
+// Under the outermost-frame model, two instructions inlined into the same
+// kernel share the kernel (outer) frame that carries the tile-IR snapshot,
+// while their heads (inner frames) differ. Merging them must PRESERVE that
+// shared outer-frame layer (MergeLocPair threads per-frame layers), so the
+// merged instruction still resolves to the kernel's tile-IR line.
+TEST(DebugLocTest, MergedLocationOuterFrameLayerPreserved) {
+  LLVMContext Ctx;
+  auto M = std::make_unique<Module>("MyModule", Ctx);
+  DIBuilder DIB(*M);
+  DIFile *SrcF = DIB.createFile("kernel.py", "/k");
+  DIFile *IntF = DIB.createFile("kernel.tileir", ".");
+  DICompileUnit *CU =
+      DIB.createCompileUnit(dwarf::DW_LANG_C, SrcF, "tile", false, "", 0);
+  DISubprogram *KernelSP = DIB.createFunction(
+      CU, "kernel", "", SrcF, 10, DIB.createSubroutineType({}), 10,
+      DINode::FlagZero, DISubprogram::SPFlagDefinition);
+  DISubprogram *CalleeSP = DIB.createFunction(
+      CU, "helper", "", SrcF, 5, DIB.createSubroutineType({}), 5,
+      DINode::FlagZero, DISubprogram::SPFlagDefinition);
+
+  MDString *Kind = MDString::get(Ctx, "tile ir");
+  DILayerLoc *Layer = DILayerLoc::get(Ctx, Kind, IntF, 100, 1);
+  DILayerLocList *KernelLayers = DILayerLocList::get(Ctx, {Layer});
+
+  // The kernel (outer) frame carries the snapshot layer.
+  DILocation *KernelFrame =
+      DILocation::get(Ctx, 50, 1, KernelSP, /*InlinedAt=*/nullptr,
+                      /*ImplicitCode=*/false, /*AtomGroup=*/0, /*AtomRank=*/0,
+                      /*IRLayers=*/KernelLayers);
+
+  // Two instructions inlined into that kernel frame: same callee, different
+  // head lines, no layer of their own.
+  DILocation *LocA = DILocation::get(Ctx, 10, 3, CalleeSP, KernelFrame);
+  DILocation *LocB = DILocation::get(Ctx, 11, 5, CalleeSP, KernelFrame);
+
+  DILocation *Merged = DILocation::getMergedLocation(LocA, LocB);
+  ASSERT_NE(Merged, nullptr);
+  // The merged head has no layer of its own...
+  EXPECT_EQ(Merged->getRawIRLayers(), nullptr);
+  // ...but the shared outer (kernel) frame, and its snapshot layer, survives so
+  // the emission walk still resolves to the kernel's tile-IR line.
+  DILocation *MergedOuter = Merged->getInlinedAt();
+  ASSERT_NE(MergedOuter, nullptr);
+  EXPECT_EQ(MergedOuter->getScope(), KernelSP);
+  EXPECT_EQ(MergedOuter->getIRLayers(), KernelLayers);
+}
+
+// Rebuilding a location with a new discriminator must carry `irlayers` over:
+// the discriminator says nothing about the intermediate-IR position. This is
+// the path AddDiscriminators, SampleProfile and loop unrolling rebuild through.
+TEST(DebugLocTest, CloneWithDiscriminatorPreservesLayers) {
+  LLVMContext Ctx;
+  auto M = std::make_unique<Module>("MyModule", Ctx);
+  DIBuilder DIB(*M);
+  DIFile *F = DIB.createFile("source.cu", "/");
+  DIFile *IntF = DIB.createFile("intermediate.tileir", ".");
+  DICompileUnit *CU =
+      DIB.createCompileUnit(dwarf::DW_LANG_C, F, "test", false, "", 0);
+  DISubprogram *SP =
+      DIB.createFunction(CU, "foo", "", F, 1, DIB.createSubroutineType({}), 1,
+                         DINode::FlagZero, DISubprogram::SPFlagDefinition);
+
+  DILayerLoc *Layer =
+      DILayerLoc::get(Ctx, MDString::get(Ctx, "tile ir"), IntF, 42, 5);
+  DILayerLocList *List = DILayerLocList::get(Ctx, {Layer});
+  DILocation *Loc =
+      DILocation::get(Ctx, 10, 5, SP, /*InlinedAt=*/nullptr,
+                      /*ImplicitCode=*/false, /*AtomGroup=*/0, /*AtomRank=*/0,
+                      /*IRLayers=*/List);
+  ASSERT_EQ(Loc->getIRLayers(), List);
+
+  std::optional<const DILocation *> Cloned = Loc->cloneWithBaseDiscriminator(7);
+  ASSERT_TRUE(Cloned.has_value());
+  ASSERT_NE(*Cloned, Loc);
+  EXPECT_EQ((*Cloned)->getBaseDiscriminator(), 7u);
+  EXPECT_EQ((*Cloned)->getIRLayers(), List);
+}
+
+// `distinct !DILayerLoc` is valid IR, so two structurally identical layers can
+// be different pointers. Merging must still recognize them as the same layer.
+TEST(DebugLocTest, MergedLocationDistinctLayersCompareStructurally) {
+  LLVMContext Ctx;
+  auto M = std::make_unique<Module>("MyModule", Ctx);
+  DIBuilder DIB(*M);
+  DIFile *F = DIB.createFile("source.cu", "/");
+  DIFile *IntF = DIB.createFile("intermediate.tileir", ".");
+  DICompileUnit *CU =
+      DIB.createCompileUnit(dwarf::DW_LANG_C, F, "test", false, "", 0);
+  DISubprogram *SP =
+      DIB.createFunction(CU, "foo", "", F, 1, DIB.createSubroutineType({}), 1,
+                         DINode::FlagZero, DISubprogram::SPFlagDefinition);
+
+  MDString *Kind = MDString::get(Ctx, "tile ir");
+  // Same fields, but each side holds its own `distinct` node, so the two shared
+  // layers are unequal pointers.
+  DILayerLoc *SharedA = DILayerLoc::getDistinct(Ctx, Kind, IntF, 100, 1);
+  DILayerLoc *SharedB = DILayerLoc::getDistinct(Ctx, Kind, IntF, 100, 1);
+  ASSERT_NE(SharedA, SharedB);
+  DILayerLoc *ExtraA = DILayerLoc::get(Ctx, Kind, IntF, 111, 1);
+  DILayerLoc *ExtraB = DILayerLoc::get(Ctx, Kind, IntF, 222, 1);
+
+  auto Layered = [&](ArrayRef<Metadata *> Layers) {
+    return DILocation::get(Ctx, 10, 5, SP, /*InlinedAt=*/nullptr,
+                           /*ImplicitCode=*/false, /*AtomGroup=*/0,
+                           /*AtomRank=*/0,
+                           /*IRLayers=*/DILayerLocList::get(Ctx, Layers));
+  };
+  DILocation *Loc1 = Layered({SharedA, ExtraA});
+  DILocation *Loc2 = Layered({SharedB, ExtraB});
+
+  DILocation *Merged = DILocation::getMergedLocation(Loc1, Loc2);
+  ASSERT_NE(Merged, nullptr);
+  // The structurally equal distinct layer survives; the divergent ones do not.
+  ASSERT_NE(Merged->getIRLayers(), nullptr);
+  ASSERT_EQ(Merged->getNumLayers(), 1u);
+  EXPECT_EQ(Merged->getLayer(0), SharedA);
+}
+
+// The layer nodes behind `irlayers` are uniqued MDNodes: structurally
+// identical DILayerLoc / DILayerLocList values map to the same pointer.
+TEST(DebugLocTest, IntermediateLocLayerUniquing) {
+  LLVMContext Ctx;
+  auto M = std::make_unique<Module>("MyModule", Ctx);
+  DIBuilder DIB(*M);
+  DIFile *IntF = DIB.createFile("intermediate.mlir", "/");
+  MDString *Kind = MDString::get(Ctx, "TileIR");
+
+  // Two structurally-identical DILayerLoc::get calls return the same node.
+  DILayerLoc *L1 = DILayerLoc::get(Ctx, Kind, IntF, 100, 5);
+  DILayerLoc *L2 = DILayerLoc::get(Ctx, Kind, IntF, 100, 5);
+  EXPECT_EQ(L1, L2);
+
+  // Any differing field yields a distinct node.
+  DILayerLoc *L3 = DILayerLoc::get(Ctx, Kind, IntF, 101, 5);
+  EXPECT_NE(L1, L3);
+
+  // DILayerLocList uniques on its operands as well.
+  DILayerLocList *List1 = DILayerLocList::get(Ctx, {L1});
+  DILayerLocList *List2 = DILayerLocList::get(Ctx, {L2}); // {L2} == {L1}
+  EXPECT_EQ(List1, List2);
+
+  DILayerLocList *List3 = DILayerLocList::get(Ctx, {L1, L3});
+  EXPECT_NE(List1, List3);
+}
+
+TEST(DebugLocTest, PrintIntermediateLocWithInlinedAt) {
+  LLVMContext Ctx;
+  auto M = std::make_unique<Module>("MyModule", Ctx);
+  DIBuilder DIB(*M);
+  DIFile *SrcF = DIB.createFile("caller.py", "/src");
+  DIFile *CalleeF = DIB.createFile("callee.py", "/src");
+  DIFile *IntF = DIB.createFile("callee.tileir", "/ir");
+  DICompileUnit *CU =
+      DIB.createCompileUnit(dwarf::DW_LANG_C, SrcF, "test", false, "", 0);
+  DISubprogram *CallerSP = DIB.createFunction(
+      CU, "caller", "", SrcF, 1, DIB.createSubroutineType({}), 1,
+      DINode::FlagZero, DISubprogram::SPFlagDefinition);
+  DISubprogram *CalleeSP = DIB.createFunction(
+      CU, "callee", "", CalleeF, 1, DIB.createSubroutineType({}), 1,
+      DINode::FlagZero, DISubprogram::SPFlagDefinition);
+
+  // A DILocation carrying BOTH an inlinedAt chain and an irlayers operand.
+  DILocation *CallSiteLoc = DILocation::get(Ctx, 50, 1, CallerSP);
+  MDString *Kind = MDString::get(Ctx, "TileIR");
+  DILayerLoc *Layer = DILayerLoc::get(Ctx, Kind, IntF, 100, 5);
+  DILayerLocList *Layers = DILayerLocList::get(Ctx, {Layer});
+  DILocation *Loc =
+      DILocation::get(Ctx, 10, 3, CalleeSP, CallSiteLoc,
+                      /*ImplicitCode=*/false, /*AtomGroup=*/0, /*AtomRank=*/0,
+                      /*IRLayers=*/Layers);
+
+  // Print the DILocation node itself so the typed operands are rendered.
+  std::string Result;
+  raw_string_ostream OS(Result);
+  Loc->print(OS, M.get());
+
+  // The printed node names both trailing operands.
+  EXPECT_NE(Result.find("!DILocation("), std::string::npos) << Result;
+  EXPECT_NE(Result.find("inlinedAt:"), std::string::npos) << Result;
+  EXPECT_NE(Result.find("irlayers:"), std::string::npos) << Result;
+
+  // The human-readable DebugLoc form still prints the source coordinate and its
+  // inlinedAt chain.
+  std::string DLResult;
+  raw_string_ostream DLOS(DLResult);
+  DebugLoc(Loc).print(DLOS);
+  EXPECT_NE(DLResult.find("callee.py:10:3"), std::string::npos) << DLResult;
+  EXPECT_NE(DLResult.find("@["), std::string::npos) << DLResult;
+  EXPECT_NE(DLResult.find("caller.py:50:1"), std::string::npos) << DLResult;
+}
+
 } // end namespace

>From 41930ab61672c70103cb86437a356c5936aab9e9 Mon Sep 17 00:00:00 2001
From: Alexander Yermolovich <ayermolovich at nvidia.com>
Date: Tue, 11 Aug 2026 20:04:51 +0000
Subject: [PATCH 2/3] [DebugInfo] Preserve DILocation irlayers across inlining
 and remapping

A DILocation carrying `irlayers` is rebuilt in several places that copy the
other fields one by one, so the new operand was silently dropped: the inliner's
appendInlinedAt, ValueMapper's RemapSourceAtom, cloneWithDiscriminator (the
path AddDiscriminators, SampleProfile and loop unrolling rebuild through), and
the location rebuilds in strip-nonlinetable-debuginfo. Each now carries the
operand over.

The intermediate coordinate belongs to whichever frame was outermost when the
producer took its snapshot, so a later inline turns that frame into an inner
frame of a deeper chain. Its layers have to survive that, since the backend
finds them by walking outward from the head to the first layer-bearing frame.

strip-nonlinetable-debuginfo needed one further fix: an intermediate layer is
line-table data, so it survives the downgrade to line-tables-only, but the
pass's fallback remapping would have rebuilt DILayerLoc and DILayerLocList as
generic MDTuples. Both now have a typed passthrough.
---
 llvm/lib/IR/DebugInfo.cpp                     | 23 +++++--
 llvm/lib/Transforms/Utils/InlineFunction.cpp  |  9 ++-
 llvm/lib/Transforms/Utils/ValueMapper.cpp     |  7 +-
 .../Inline/intermediate-loc-inline.ll         | 65 +++++++++++++++++++
 .../strip-nonlinetable-debuginfo-irlayers.ll  | 34 ++++++++++
 5 files changed, 129 insertions(+), 9 deletions(-)
 create mode 100644 llvm/test/Transforms/Inline/intermediate-loc-inline.ll
 create mode 100644 llvm/test/Transforms/Util/strip-nonlinetable-debuginfo-irlayers.ll

diff --git a/llvm/lib/IR/DebugInfo.cpp b/llvm/lib/IR/DebugInfo.cpp
index e164ec54ead60..540d7a43a3fee 100644
--- a/llvm/lib/IR/DebugInfo.cpp
+++ b/llvm/lib/IR/DebugInfo.cpp
@@ -851,11 +851,16 @@ class DebugTypeInfoRemoval {
   DILocation *getReplacementMDLocation(DILocation *MLD) {
     auto *Scope = map(MLD->getScope());
     auto *InlinedAt = map(MLD->getInlinedAt());
+    // Intermediate-IR layers are line-table data (a line/column in a DIFile),
+    // so they survive the downgrade to line-tables-only.
+    Metadata *IRLayers = map(MLD->getRawIRLayers());
     if (MLD->isDistinct())
-      return DILocation::getDistinct(MLD->getContext(), MLD->getLine(),
-                                     MLD->getColumn(), Scope, InlinedAt);
+      return DILocation::getDistinct(
+          MLD->getContext(), MLD->getLine(), MLD->getColumn(), Scope, InlinedAt,
+          /*ImplicitCode=*/false, /*AtomGroup=*/0, /*AtomRank=*/0, IRLayers);
     return DILocation::get(MLD->getContext(), MLD->getLine(), MLD->getColumn(),
-                           Scope, InlinedAt);
+                           Scope, InlinedAt, /*ImplicitCode=*/false,
+                           /*AtomGroup=*/0, /*AtomRank=*/0, IRLayers);
   }
 
   /// Create a new generic MDNode, to replace the one given
@@ -887,6 +892,12 @@ class DebugTypeInfoRemoval {
         return getReplacementCU(CU);
       if (isa<DIFile>(N))
         return N;
+      // Intermediate-IR layer nodes hold only a kind string and a DIFile, so
+      // none of their content is part of the type hierarchy being stripped. The
+      // generic fallback below would rebuild them as plain MDTuples, which
+      // DILocation::getIRLayers() cannot cast.
+      if (isa<DILayerLoc>(N) || isa<DILayerLocList>(N))
+        return N;
       if (auto *MDLB = dyn_cast<DILexicalBlockBase>(N))
         // Remap to our referenced scope (recursively).
         return mapNode(MDLB->getScope());
@@ -990,8 +1001,12 @@ bool llvm::stripNonLineTableDebugInfo(Module &M) {
           MDNode *InlinedAt = DL.getInlinedAt();
           Scope = remap(Scope);
           InlinedAt = remap(InlinedAt);
+          // Layers need no remapping (Mapper maps them to themselves), but they
+          // do need carrying over -- they are line-table data.
           return DILocation::get(M.getContext(), DL.getLine(), DL.getCol(),
-                                 Scope, InlinedAt);
+                                 Scope, InlinedAt, /*ImplicitCode=*/false,
+                                 /*AtomGroup=*/0, /*AtomRank=*/0,
+                                 DL.getRawIRLayers());
         };
 
         if (I.getDebugLoc() != DebugLoc())
diff --git a/llvm/lib/Transforms/Utils/InlineFunction.cpp b/llvm/lib/Transforms/Utils/InlineFunction.cpp
index 45454002a78e2..b3ab81505df85 100644
--- a/llvm/lib/Transforms/Utils/InlineFunction.cpp
+++ b/llvm/lib/Transforms/Utils/InlineFunction.cpp
@@ -1938,7 +1938,8 @@ static DebugLoc inlineDebugLoc(DebugLoc OrigDL, DILocation *InlinedAt,
   auto IA = DebugLoc::appendInlinedAt(OrigDL, InlinedAt, Ctx, IANodes);
   return DILocation::get(Ctx, OrigDL.getLine(), OrigDL.getCol(),
                          OrigDL.getScope(), IA, OrigDL.isImplicitCode(),
-                         OrigDL->getAtomGroup(), OrigDL->getAtomRank());
+                         OrigDL->getAtomGroup(), OrigDL->getAtomRank(),
+                         OrigDL.getRawIRLayers());
 }
 
 /// Update inlined instructions' line numbers to
@@ -1960,9 +1961,13 @@ static void fixupLineNumbers(Function *Fn, Function::iterator FI,
 
   // Create a unique call site, not to be confused with any other call from the
   // same location.
+  // Preserve the call site's irlayers so the (outermost) inlined-at
+  // frame keeps its intermediate-IR snapshot instead of being stripped.
   InlinedAtNode = DILocation::getDistinct(
       Ctx, InlinedAtNode->getLine(), InlinedAtNode->getColumn(),
-      InlinedAtNode->getScope(), InlinedAtNode->getInlinedAt());
+      InlinedAtNode->getScope(), InlinedAtNode->getInlinedAt(),
+      /*ImplicitCode=*/false, /*AtomGroup=*/0, /*AtomRank=*/0,
+      InlinedAtNode->getRawIRLayers());
 
   // Cache the inlined-at nodes as they're built so they are reused, without
   // this every instruction's inlined-at chain would become distinct from each
diff --git a/llvm/lib/Transforms/Utils/ValueMapper.cpp b/llvm/lib/Transforms/Utils/ValueMapper.cpp
index 22230c352bf01..ee506afe06f12 100644
--- a/llvm/lib/Transforms/Utils/ValueMapper.cpp
+++ b/llvm/lib/Transforms/Utils/ValueMapper.cpp
@@ -1332,8 +1332,9 @@ void llvm::RemapSourceAtom(Instruction *I, ValueToValueMapTy &VM) {
   AtomGroup = R->second;
 
   // Remap the atom group and copy all other fields.
-  DILocation *New = DILocation::get(
-      I->getContext(), DL.getLine(), DL.getCol(), DL.getScope(),
-      DL.getInlinedAt(), DL.isImplicitCode(), AtomGroup, DL->getAtomRank());
+  DILocation *New =
+      DILocation::get(I->getContext(), DL.getLine(), DL.getCol(), DL.getScope(),
+                      DL.getInlinedAt(), DL.isImplicitCode(), AtomGroup,
+                      DL->getAtomRank(), DL.getRawIRLayers());
   I->setDebugLoc(New);
 }
diff --git a/llvm/test/Transforms/Inline/intermediate-loc-inline.ll b/llvm/test/Transforms/Inline/intermediate-loc-inline.ll
new file mode 100644
index 0000000000000..228e1d4a725b5
--- /dev/null
+++ b/llvm/test/Transforms/Inline/intermediate-loc-inline.ll
@@ -0,0 +1,65 @@
+; RUN: opt %s -passes='always-inline' -S | FileCheck %s
+
+;; This is the shape the tile compiler emits: the front end inlined `helper`
+;; into `kernel`, THEN captured the tile-IR snapshot, so the snapshot (irlayers)
+;; sits on the OUTER `kernel` frame -- the instruction head (`helper`) carries
+;; no layer of its own. The layer lives on whichever DILocation
+;; frame was outermost when the snapshot was taken (here, `kernel`).
+;;
+;; `kernel` is then inlined into `caller` by the LLVM inliner -- an inline AFTER
+;; the snapshot. The `kernel` frame becomes an INNER frame of the deeper chain,
+;; and its irlayers must be PRESERVED, not stripped (appendInlinedAt /
+;; InlinedAtNode). The NVPTX backend later finds this layer by
+;; walking head -> outward to the first layer-bearing frame.
+
+define void @kernel(ptr %p) alwaysinline !dbg !6 {
+  store ptr null, ptr %p, align 8, !dbg !9
+  ret void, !dbg !8
+}
+
+define void @caller(ptr %p) !dbg !12 {
+  call void @kernel(ptr %p), !dbg !15
+  ret void, !dbg !16
+}
+
+; CHECK-LABEL: define void @caller
+; CHECK: store ptr null, ptr %p,{{.*}} !dbg ![[INST:[0-9]+]]
+
+;; The inlined store keeps the helper (head) source loc; its inlinedAt now points
+;; at the kernel frame.
+; CHECK-DAG: ![[INST]] = !DILocation(line: 6, column: 1, scope: ![[HELPER:[0-9]+]], inlinedAt: ![[KFRAME:[0-9]+]])
+;; The kernel frame is an INNER frame now (inlinedAt the caller call site) and
+;; RETAINS its snapshot layer -- the whole point of the outermost-frame model.
+; CHECK-DAG: ![[KFRAME]] = distinct !DILocation(line: 11, column: 1, scope: ![[KERNEL:[0-9]+]], inlinedAt: ![[CS:[0-9]+]], irlayers: ![[LIST:[0-9]+]])
+;; The appended outermost frame is the caller's call site, with no layer of its own.
+; CHECK-DAG: ![[CS]] = distinct !DILocation(line: 15, column: 1, scope: ![[CALLER:[0-9]+]])
+; CHECK-DAG: ![[LIST]] = !DILayerLocList(![[LAYER:[0-9]+]])
+; CHECK-DAG: ![[LAYER]] = !DILayerLoc(line: 100, column: 1, file: {{![0-9]+}}, kind: "tile ir")
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, producer: "tile", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "kernel.py", directory: "/k")
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !DISubroutineType(types: !5)
+!5 = !{null}
+
+;; kernel (outer-frame scope) and the front-end-inlined helper (head scope).
+!6 = distinct !DISubprogram(name: "kernel", scope: !1, file: !1, line: 10, type: !4, spFlags: DISPFlagDefinition, unit: !0)
+!7 = distinct !DISubprogram(name: "helper", scope: !1, file: !1, line: 5, type: !4, spFlags: DISPFlagDefinition, unit: !0)
+!8 = !DILocation(line: 10, column: 1, scope: !6)
+
+;; tile-IR snapshot file + the snapshot layer.
+!18 = !DIFile(filename: "kernel.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")
+!19 = !DILayerLoc(line: 100, column: 1, file: !18, kind: "tile ir")
+!17 = !DILayerLocList(!19)
+
+;; The kernel (outer) frame carries the snapshot; the helper (head) does not.
+!10 = !DILocation(line: 11, column: 1, scope: !6, irlayers: !17)
+!9 = !DILocation(line: 6, column: 1, scope: !7, inlinedAt: !10)
+
+;; caller.
+!12 = distinct !DISubprogram(name: "caller", scope: !1, file: !1, line: 20, type: !4, spFlags: DISPFlagDefinition, unit: !0)
+!15 = !DILocation(line: 15, column: 1, scope: !12)
+!16 = !DILocation(line: 16, column: 1, scope: !12)
diff --git a/llvm/test/Transforms/Util/strip-nonlinetable-debuginfo-irlayers.ll b/llvm/test/Transforms/Util/strip-nonlinetable-debuginfo-irlayers.ll
new file mode 100644
index 0000000000000..15771e8db0cc3
--- /dev/null
+++ b/llvm/test/Transforms/Util/strip-nonlinetable-debuginfo-irlayers.ll
@@ -0,0 +1,34 @@
+; RUN: opt -passes=strip-nonlinetable-debuginfo -S %s | FileCheck %s
+;;
+;; strip-nonlinetable-debuginfo downgrades -g metadata to line-tables-only. An
+;; intermediate-IR layer is line-table data -- a line/column in a DIFile -- so it
+;; must survive, and DILayerLoc/DILayerLocList must keep their types rather than
+;; being rebuilt as generic MDTuples by the pass's fallback remapping.
+
+define void @f(ptr %p) !dbg !5 {
+  store ptr null, ptr %p, align 8, !dbg !20
+  ret void, !dbg !21
+}
+
+; CHECK: store ptr null, ptr %p, align 8, !dbg ![[DBG:[0-9]+]]
+; CHECK-DAG: ![[DBG]] = !DILocation(line: 2, column: 5, scope: !{{[0-9]+}}, irlayers: ![[LIST:[0-9]+]])
+; CHECK-DAG: ![[LIST]] = !DILayerLocList(![[LAYER:[0-9]+]])
+; CHECK-DAG: ![[LAYER]] = !DILayerLoc(line: 42, column: 5, file: ![[INTF:[0-9]+]], kind: "tile ir")
+; CHECK-DAG: ![[INTF]] = !DIFile(filename: "kernel.tileir", directory: ".")
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cu", directory: "/test")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !DISubroutineType(types: !{null})
+!5 = distinct !DISubprogram(name: "f", scope: !1, file: !1, line: 1, type: !4, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+
+!10 = !DIFile(filename: "kernel.tileir", directory: ".")
+!11 = !DILayerLoc(line: 42, column: 5, file: !10, kind: "tile ir")
+!12 = !DILayerLocList(!11)
+
+!20 = !DILocation(line: 2, column: 5, scope: !5, irlayers: !12)
+!21 = !DILocation(line: 3, column: 1, scope: !5)

>From efa26718a11e405a08e7bc4d118db14b74d187b8 Mon Sep 17 00:00:00 2001
From: Alexander Yermolovich <ayermolovich at nvidia.com>
Date: Tue, 11 Aug 2026 20:09:21 +0000
Subject: [PATCH 3/3] [NVPTX] Emit .loc_intermediate and
 .nv_intermediate_source_section

Lower DILocation's `irlayers` into PTX so a debugger can step through the
intermediate IR a kernel was compiled from. Each layer on an instruction's
location emits a secondary .loc_intermediate directive next to the primary
.loc, and the intermediate source text carried on DIFile.source is emitted once
per file in a .nv_intermediate_source_section of .code_blocks.

MCStreamer::emitDwarfLocDirective gains a defaulted LocOpcode parameter so a
target can select the directive mnemonic. It affects textual output only:
MCAsmStreamer prints it, while the object streamers record the location in the
line table and ignore it, so a non-default mnemonic is dropped when not
emitting assembly.

Layers are read off the first (innermost) frame in the inlined-at chain that
carries them, which is the most specific intermediate origin for that PC.
Secondary .file entries are named by the DIFile's checksum digest, since ptxas
keys the stored source by that name; without a checksum the directory and
filename are hashed instead. A layer whose file carries no source text is
dropped entirely rather than emitting a .loc_intermediate the consumer cannot
resolve to a .code_block.
---
 llvm/docs/NVPTXUsage.md                       | 57 +++++++++++
 llvm/include/llvm/MC/MCObjectStreamer.h       |  3 +-
 llvm/include/llvm/MC/MCStreamer.h             | 10 +-
 llvm/lib/MC/MCAsmStreamer.cpp                 |  9 +-
 llvm/lib/MC/MCObjectStreamer.cpp              | 10 +-
 llvm/lib/MC/MCStreamer.cpp                    |  3 +-
 llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp     | 10 ++
 llvm/lib/Target/NVPTX/NVPTXDwarfDebug.cpp     | 94 +++++++++++++++++++
 llvm/lib/Target/NVPTX/NVPTXDwarfDebug.h       | 29 ++++++
 .../intermediate-checksum-secondary-file.ll   | 43 +++++++++
 .../intermediate-loc-crossmodule-shadow.ll    | 78 +++++++++++++++
 .../NVPTX/intermediate-loc-dedup-gate.ll      | 68 ++++++++++++++
 .../NVPTX/intermediate-loc-inline-shadow.ll   | 85 +++++++++++++++++
 .../NVPTX/intermediate-loc-multi-layer.ll     | 62 ++++++++++++
 llvm/test/DebugInfo/NVPTX/intermediate-loc.ll | 76 +++++++++++++++
 ...intermediate-source-section-determinism.ll | 64 +++++++++++++
 .../intermediate-source-section-empty.ll      | 45 +++++++++
 .../intermediate-source-section-mixed.ll      | 62 ++++++++++++
 ...intermediate-source-section-no-checksum.ll | 51 ++++++++++
 ...ermediate-source-section-shared-filenum.ll | 58 ++++++++++++
 .../NVPTX/intermediate-source-section.ll      | 81 ++++++++++++++++
 21 files changed, 984 insertions(+), 14 deletions(-)
 create mode 100644 llvm/test/DebugInfo/NVPTX/intermediate-checksum-secondary-file.ll
 create mode 100644 llvm/test/DebugInfo/NVPTX/intermediate-loc-crossmodule-shadow.ll
 create mode 100644 llvm/test/DebugInfo/NVPTX/intermediate-loc-dedup-gate.ll
 create mode 100644 llvm/test/DebugInfo/NVPTX/intermediate-loc-inline-shadow.ll
 create mode 100644 llvm/test/DebugInfo/NVPTX/intermediate-loc-multi-layer.ll
 create mode 100644 llvm/test/DebugInfo/NVPTX/intermediate-loc.ll
 create mode 100644 llvm/test/DebugInfo/NVPTX/intermediate-source-section-determinism.ll
 create mode 100644 llvm/test/DebugInfo/NVPTX/intermediate-source-section-empty.ll
 create mode 100644 llvm/test/DebugInfo/NVPTX/intermediate-source-section-mixed.ll
 create mode 100644 llvm/test/DebugInfo/NVPTX/intermediate-source-section-no-checksum.ll
 create mode 100644 llvm/test/DebugInfo/NVPTX/intermediate-source-section-shared-filenum.ll
 create mode 100644 llvm/test/DebugInfo/NVPTX/intermediate-source-section.ll

diff --git a/llvm/docs/NVPTXUsage.md b/llvm/docs/NVPTXUsage.md
index 2541d504c6e10..502fd02aa46d6 100644
--- a/llvm/docs/NVPTXUsage.md
+++ b/llvm/docs/NVPTXUsage.md
@@ -154,6 +154,63 @@ Example: 32-bit PTX for CUDA Driver API: `nvptx-nvidia-cuda`
 
 Example: 64-bit PTX for CUDA Driver API: `nvptx64-nvidia-cuda`
 
+### Multi-Level Line Tables
+
+A GPU program is often lowered through one or more intermediate IRs before
+reaching LLVM IR, and a tool may want to map an address back to a position in
+each of them, not just to the original source. A `DILocation` can carry those
+extra positions in its `irlayers:` field, which points at a `DILayerLocList` of
+`DILayerLoc` entries — see
+[LangRef](LangRef.md#dilayerloc). NVPTX is currently the only
+consumer; other targets ignore the field.
+
+For each layer on an instruction's location, the backend emits a secondary
+directive immediately after the primary `.loc`:
+
+```text
+.loc 1 2 5                  // primary source position
+.loc_intermediate 2 100 10  // position in the intermediate IR
+```
+
+The layer's `DIFile` becomes an ordinary `.file` entry, so `.loc_intermediate`
+refers to it by file number exactly as `.loc` does.
+
+The layer's intermediate IR text travels with it, in a
+`.nv_intermediate_source_section`, so a consumer can show that IR without needing
+the file on disk:
+
+```text
+.nv_intermediate_source_section {
+  .code_block {
+    .ir_name: "tile ir"
+    .sourceFileName: 2
+    .source_begin
+...intermediate IR text...
+.source_end
+  }
+}
+```
+
+`.ir_name` comes from `DILayerLoc`'s `kind:` and `.sourceFileName` is the file
+number the `.code_block` describes. `ptxas` stores that text keyed by the
+secondary file's *name*, so the name must be unique per file rather than
+human-readable: the backend uses the `DIFile`'s checksum digest when it has one
+and a hash of the file's path otherwise.
+
+The text comes from the layer `DIFile`'s `source:` field, and a layer is only
+emitted when that field is present. A `.loc_intermediate` pointing at a file with
+no `.code_block` is not accepted, so rather than emit a reference that cannot be
+resolved, a layer whose `DIFile` has no `source:` is dropped entirely — it
+contributes no `.loc_intermediate`, no `.file` entry, and no `.code_block`. A
+producer that wants intermediate positions in the output must therefore also
+carry the intermediate text.
+
+Layer positions are preserved across inlining and location merging. A layer
+attaches to the inline frame it originated from, and emission resolves an
+instruction to the nearest enclosing frame that carries one, so code inlined into
+a layered region is attributed to the enclosing layer position rather than
+losing its intermediate attribution.
+
 (nvptx-arch-hierarchy)=
 
 ## NVPTX Architecture Hierarchy and Ordering
diff --git a/llvm/include/llvm/MC/MCObjectStreamer.h b/llvm/include/llvm/MC/MCObjectStreamer.h
index 7f5875c90d215..7497c17e3b03e 100644
--- a/llvm/include/llvm/MC/MCObjectStreamer.h
+++ b/llvm/include/llvm/MC/MCObjectStreamer.h
@@ -153,7 +153,8 @@ class LLVM_ABI MCObjectStreamer : public MCStreamer {
   void emitDwarfLocDirective(unsigned FileNo, unsigned Line, unsigned Column,
                              unsigned Flags, unsigned Isa,
                              unsigned Discriminator, StringRef FileName,
-                             StringRef Comment = {}) override;
+                             StringRef Comment = {},
+                             StringRef LocOpcode = {}) override;
   void emitDwarfAdvanceLineAddr(int64_t LineDelta, const MCSymbol *LastLabel,
                                 const MCSymbol *Label,
                                 unsigned PointerSize) override;
diff --git a/llvm/include/llvm/MC/MCStreamer.h b/llvm/include/llvm/MC/MCStreamer.h
index 614b8d79c5da4..78ced7b50b4f0 100644
--- a/llvm/include/llvm/MC/MCStreamer.h
+++ b/llvm/include/llvm/MC/MCStreamer.h
@@ -918,11 +918,17 @@ class LLVM_ABI MCStreamer {
 
   /// This implements the DWARF2 '.loc fileno lineno ...' assembler
   /// directive.
+  ///
+  /// \p LocOpcode selects the directive mnemonic, allowing a target to emit a
+  /// variant such as NVPTX's `.loc_intermediate`. It only affects textual
+  /// output: MCAsmStreamer prints it, while this implementation and
+  /// MCObjectStreamer record the location in the line table and ignore it, so a
+  /// non-default mnemonic is silently dropped when not emitting assembly.
   virtual void emitDwarfLocDirective(unsigned FileNo, unsigned Line,
                                      unsigned Column, unsigned Flags,
                                      unsigned Isa, unsigned Discriminator,
-                                     StringRef FileName,
-                                     StringRef Comment = {});
+                                     StringRef FileName, StringRef Comment = {},
+                                     StringRef LocOpcode = ".loc");
 
   /// This is same as emitDwarfLocDirective, except it has the capability to
   /// add inlined_at information.
diff --git a/llvm/lib/MC/MCAsmStreamer.cpp b/llvm/lib/MC/MCAsmStreamer.cpp
index 2dd964edb738a..9631cc636cad8 100644
--- a/llvm/lib/MC/MCAsmStreamer.cpp
+++ b/llvm/lib/MC/MCAsmStreamer.cpp
@@ -409,7 +409,8 @@ class MCAsmStreamer final : public MCAsmBaseStreamer {
   void emitDwarfLocDirective(unsigned FileNo, unsigned Line, unsigned Column,
                              unsigned Flags, unsigned Isa,
                              unsigned Discriminator, StringRef FileName,
-                             StringRef Location = {}) override;
+                             StringRef Location = {},
+                             StringRef LocOpcode = ".loc") override;
   void emitDwarfLocLabelDirective(SMLoc Loc, StringRef Name) override;
 
   /// This is same as emitDwarfLocDirective, except also emits inlined function
@@ -1894,8 +1895,8 @@ void MCAsmStreamer::emitDwarfLocDirectiveSuffix(unsigned FileNo, unsigned Line,
 void MCAsmStreamer::emitDwarfLocDirective(unsigned FileNo, unsigned Line,
                                           unsigned Column, unsigned Flags,
                                           unsigned Isa, unsigned Discriminator,
-                                          StringRef FileName,
-                                          StringRef Comment) {
+                                          StringRef FileName, StringRef Comment,
+                                          StringRef LocOpcode) {
   // If target doesn't support .loc/.file directive, we need to record the lines
   // same way like we do in object mode.
   if (MAI->isAIX()) {
@@ -1908,7 +1909,7 @@ void MCAsmStreamer::emitDwarfLocDirective(unsigned FileNo, unsigned Line,
   }
 
   // Emit the basic .loc directive.
-  OS << "\t.loc\t" << FileNo << " " << Line << " " << Column;
+  OS << "\t" << LocOpcode << "\t" << FileNo << " " << Line << " " << Column;
 
   // Emit common suffix (flags, comment, EOL, parent call).
   emitDwarfLocDirectiveSuffix(FileNo, Line, Column, Flags, Isa, Discriminator,
diff --git a/llvm/lib/MC/MCObjectStreamer.cpp b/llvm/lib/MC/MCObjectStreamer.cpp
index 7a64fbbd66712..35f7e98869bf9 100644
--- a/llvm/lib/MC/MCObjectStreamer.cpp
+++ b/llvm/lib/MC/MCObjectStreamer.cpp
@@ -514,12 +514,10 @@ void MCObjectStreamer::emitInstToFragment(const MCInst &Inst,
   newFragment();
 }
 
-void MCObjectStreamer::emitDwarfLocDirective(unsigned FileNo, unsigned Line,
-                                             unsigned Column, unsigned Flags,
-                                             unsigned Isa,
-                                             unsigned Discriminator,
-                                             StringRef FileName,
-                                             StringRef Comment) {
+void MCObjectStreamer::emitDwarfLocDirective(
+    unsigned FileNo, unsigned Line, unsigned Column, unsigned Flags,
+    unsigned Isa, unsigned Discriminator, StringRef FileName, StringRef Comment,
+    StringRef LocOpcode) {
   // In case we see two .loc directives in a row, make sure the
   // first one gets a line entry.
   MCDwarfLineEntry::make(this, getCurrentSectionOnly());
diff --git a/llvm/lib/MC/MCStreamer.cpp b/llvm/lib/MC/MCStreamer.cpp
index 1d51fbc46a43c..5551b0491ad20 100644
--- a/llvm/lib/MC/MCStreamer.cpp
+++ b/llvm/lib/MC/MCStreamer.cpp
@@ -235,7 +235,8 @@ void MCStreamer::emitCFIMTETaggedFrame() {
 void MCStreamer::emitDwarfLocDirective(unsigned FileNo, unsigned Line,
                                        unsigned Column, unsigned Flags,
                                        unsigned Isa, unsigned Discriminator,
-                                       StringRef FileName, StringRef Comment) {
+                                       StringRef FileName, StringRef Comment,
+                                       StringRef LocOpcode) {
   getContext().setCurrentDwarfLoc(FileNo, Line, Column, Flags, Isa,
                                   Discriminator);
 }
diff --git a/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp b/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp
index 9f8051ebc2ed8..2fc7fba94eb04 100644
--- a/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp
+++ b/llvm/lib/Target/NVPTX/NVPTXAsmPrinter.cpp
@@ -1356,6 +1356,13 @@ bool NVPTXAsmPrinter::doFinalization(Module &M) {
     GlobalsEmitted = true;
   }
 
+  // Build the intermediate source section text while the DwarfDebug is still
+  // alive (AsmPrinter::doFinalization destroys it). It is emitted after the
+  // .file directives below so ptxas can resolve .sourceFileName references.
+  std::string IntermediateSrcSection;
+  if (auto *NVDD = static_cast<NVPTXDwarfDebug *>(getDwarfDebug()))
+    IntermediateSrcSection = NVDD->buildIntermediateSourceSection();
+
   // call doFinalization
   bool ret = AsmPrinter::doFinalization(M);
 
@@ -1373,6 +1380,9 @@ bool NVPTXAsmPrinter::doFinalization(Module &M) {
   // Output last DWARF .file directives, if any.
   TS->outputDwarfFileDirectives();
 
+  if (!IntermediateSrcSection.empty())
+    OutStreamer->emitRawText(IntermediateSrcSection);
+
   return ret;
 }
 
diff --git a/llvm/lib/Target/NVPTX/NVPTXDwarfDebug.cpp b/llvm/lib/Target/NVPTX/NVPTXDwarfDebug.cpp
index 283641d1bf516..4bb185fe01e6d 100644
--- a/llvm/lib/Target/NVPTX/NVPTXDwarfDebug.cpp
+++ b/llvm/lib/Target/NVPTX/NVPTXDwarfDebug.cpp
@@ -23,7 +23,9 @@
 #include "llvm/MC/MCContext.h"
 #include "llvm/MC/MCStreamer.h"
 #include "llvm/Support/CommandLine.h"
+#include "llvm/Support/MD5.h"
 #include "llvm/Support/NVPTXAddrSpace.h"
+#include "llvm/Support/raw_ostream.h"
 #include "llvm/Target/TargetMachine.h"
 
 using namespace llvm;
@@ -188,6 +190,98 @@ void NVPTXDwarfDebug::recordTargetSourceLine(const DebugLoc &DL,
     if (EnhancedLineinfo)
       EmittedInlinedAtLocs.insert(Current);
   }
+
+  recordIntermediateLoc(DL, Flags);
+}
+
+void NVPTXDwarfDebug::recordIntermediateLoc(const DebugLoc &DL,
+                                            unsigned Flags) {
+  // Intermediate-IR layers live on the frame that was outermost when the tile
+  // snapshot was taken; after later inlining that frame can sit anywhere in the
+  // inlined-at chain. Walk outward from the head and use the first (innermost)
+  // frame that carries layers: it is the most specific intermediate origin for
+  // this PC -- the instruction actually being executed -- and for layer-less
+  // inlined code it is the nearest enclosing frame (continuous coverage).
+  const DILocation *Loc = DL.get();
+  while (Loc && !Loc->getRawIRLayers())
+    Loc = Loc->getInlinedAt();
+  if (!Loc)
+    return;
+  DILayerLocList *Layers = Loc->getIRLayers();
+  // Emit a secondary .loc_intermediate DWARF directive for each intermediate-IR
+  // layer. Each DILayerLoc carries its file/line/column directly (no scope and
+  // no discriminator).
+  const unsigned CUID = Asm->OutStreamer->getContext().getDwarfCompileUnitID();
+  for (unsigned I = 0, E = Layers->getNumLayers(); I != E; ++I) {
+    // The verifier guarantees each entry is a non-null DILayerLoc whose file is
+    // a non-null DIFile, so no null checks are needed here.
+    const DILayerLoc *L = Layers->getLayer(I);
+    const unsigned Line = L->getLine();
+    const DIFile *OrigFile = L->getFile();
+    // Skip the DWARF "no line" sentinel (line 0 is valid IR, not a verifier
+    // invariant).
+    if (!Line)
+      continue;
+    // A .loc_intermediate is only meaningful alongside the intermediate source
+    // it points into: the consumer rejects a reference to a file that has no
+    // .code_block. So a layer whose file carries no source text is dropped here
+    // rather than emitting a dangling .loc_intermediate and .file entry.
+    std::optional<StringRef> Source = OrigFile->getSource();
+    if (!Source)
+      continue;
+    auto [CacheIt, Inserted] = IntermediateFileNums.try_emplace(OrigFile);
+    if (Inserted) {
+      // Secondary .file name. ptxas keys the .source text it stores in the
+      // cubin by this name, so it must be a unique hash rather than a readable
+      // path: the file's checksum digest if it has one, else a path hash.
+      SmallString<32> HashedName;
+      StringRef SecondaryFilename;
+      if (const auto Checksum = OrigFile->getChecksum()) {
+        SecondaryFilename = Checksum->Value;
+      } else {
+        MD5 Hash;
+        Hash.update(OrigFile->getDirectory());
+        Hash.update(OrigFile->getFilename());
+        MD5::MD5Result Result;
+        Hash.final(Result);
+        HashedName = Result.digest();
+        SecondaryFilename = HashedName;
+      }
+      DIFile *SecondaryFile = DIFile::get(Loc->getContext(), SecondaryFilename,
+                                          OrigFile->getDirectory());
+      CacheIt->second = static_cast<DwarfCompileUnit &>(*getUnits()[CUID])
+                            .getOrCreateSourceID(SecondaryFile);
+    }
+    const unsigned FileNo = CacheIt->second;
+    const unsigned Col = L->getColumn();
+    Asm->OutStreamer->emitDwarfLocDirective(
+        FileNo, Line, Col, Flags, 0, /*Discriminator=*/0,
+        OrigFile->getFilename(), "", ".loc_intermediate");
+    // First encounter wins. DIFiles that collapse onto one number share content
+    // by checksum, so their source agrees; a differing kind spelling (possible
+    // across LTO-merged producers, not a verifier error) just takes the first.
+    IntermediateFiles.insert({FileNo, {*Source, L->getKind()}});
+  }
+}
+
+std::string NVPTXDwarfDebug::buildIntermediateSourceSection() {
+  if (IntermediateFiles.empty())
+    return {};
+
+  // Every recorded file has source text (recordIntermediateLoc drops the
+  // others), so each entry produces a .code_block and the section is not empty.
+  std::string Buf;
+  raw_string_ostream OS(Buf);
+  OS << ".nv_intermediate_source_section {\n";
+  for (const auto &[FileNo, Info] : IntermediateFiles) {
+    OS << "  .code_block {\n";
+    OS << "    .ir_name: \"" << Info.Kind << "\"\n";
+    OS << "    .sourceFileName: " << FileNo << "\n";
+    OS << "    .source_begin\n" << Info.Source << "\n.source_end\n";
+    OS << "  }\n";
+  }
+  OS << "}";
+  return Buf;
 }
 
 /// NVPTX-specific debug info initialization.
diff --git a/llvm/lib/Target/NVPTX/NVPTXDwarfDebug.h b/llvm/lib/Target/NVPTX/NVPTXDwarfDebug.h
index 46b9a7e7a5206..e3672acb5b24f 100644
--- a/llvm/lib/Target/NVPTX/NVPTXDwarfDebug.h
+++ b/llvm/lib/Target/NVPTX/NVPTXDwarfDebug.h
@@ -17,9 +17,13 @@
 #define LLVM_LIB_TARGET_NVPTX_NVPTXDWARFDEBUG_H
 
 #include "../../CodeGen/AsmPrinter/DwarfCompileUnit.h"
+#include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/MapVector.h"
+#include <string>
 
 namespace llvm {
+class DIFile;
 
 /// NVPTX-specific DwarfDebug implementation.
 ///
@@ -60,6 +64,31 @@ class NVPTXDwarfDebug : public DwarfDebug {
   void recordTargetSourceLine(const DebugLoc &DL, unsigned Flags) override;
   bool shouldAttachCompileUnitRanges() const override;
   bool shouldEmitDwarfPubSections() const override { return false; }
+
+public:
+  /// Build the .nv_intermediate_source_section PTX text from the layers
+  /// collected during line emission. Returns the section as a string so the
+  /// caller can emit it after .file directives have been flushed (empty if
+  /// there is nothing to emit).
+  std::string buildIntermediateSourceSection();
+
+private:
+  /// Emit secondary .loc_intermediate directives for the intermediate-IR
+  /// layers carried on a DebugLoc's irlayers operand.
+  void recordIntermediateLoc(const DebugLoc &DL, unsigned Flags);
+
+  /// Stores source code and IR kind, per intermediate .file number, for
+  /// emission into .code_block.
+  struct IntermediateFileInfo {
+    StringRef Source;
+    StringRef Kind;
+  };
+  MapVector<unsigned, IntermediateFileInfo> IntermediateFiles;
+
+  /// Secondary .file number for each intermediate DIFile already seen, so a
+  /// repeated layer skips rebuilding the derived DIFile and re-querying the
+  /// DWARF file table.
+  DenseMap<const DIFile *, unsigned> IntermediateFileNums;
 };
 
 } // end namespace llvm
diff --git a/llvm/test/DebugInfo/NVPTX/intermediate-checksum-secondary-file.ll b/llvm/test/DebugInfo/NVPTX/intermediate-checksum-secondary-file.ll
new file mode 100644
index 0000000000000..8d32ce5b68062
--- /dev/null
+++ b/llvm/test/DebugInfo/NVPTX/intermediate-checksum-secondary-file.ll
@@ -0,0 +1,43 @@
+; RUN: llc -O0 < %s -mtriple=nvptx64-nvidia-cuda | FileCheck %s
+;;
+;; For an intermediate-IR file that carries source, the secondary .file directive
+;; must use the DIFile's *carried* checksum digest as its name: NVPTX emit reads
+;; DIFile.getChecksum()->Value directly rather than recomputing a hash of the
+;; filename. The distinctive checksum "cafebabe..." below is deliberately NOT
+;; MD5("kernel.tileir"), so if emit ever went back to hashing the filename this
+;; test would fail.
+
+define dso_local void @test_kernel(ptr noundef %v) #0 !dbg !8 {
+entry:
+  %v.addr = alloca ptr, align 8
+  store ptr %v, ptr %v.addr, align 8, !dbg !20
+  ret void, !dbg !21
+}
+
+attributes #0 = { noinline optnone }
+
+;; The intermediate .loc references a file number...
+; CHECK: .loc_intermediate [[INTFILE:[0-9]+]] 42 5
+;; ... whose .file NAME is exactly the carried checksum value, prefixed by the
+;; intermediate DIFile's directory (".").
+; CHECK: .file [[INTFILE]] ".{{/|\\\\}}cafebabecafebabecafebabecafebabe"
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cu", directory: "/test")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!8 = distinct !DISubprogram(name: "test_kernel", scope: !1, file: !1, line: 1, type: !9, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null}
+
+;; Intermediate file WITH source and a distinctive checksum that is NOT a hash of
+;; its name -- so the secondary .file name must be the carried checksum.
+!14 = !DIFile(filename: "kernel.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "cafebabecafebabecafebabecafebabe", source: "tile ir source")
+!30 = !DILayerLocList(!31)
+!31 = !DILayerLoc(line: 42, column: 5, file: !14, kind: "tile ir")
+
+!20 = !DILocation(line: 2, column: 5, scope: !8, irlayers: !30)
+!21 = !DILocation(line: 3, column: 1, scope: !8)
diff --git a/llvm/test/DebugInfo/NVPTX/intermediate-loc-crossmodule-shadow.ll b/llvm/test/DebugInfo/NVPTX/intermediate-loc-crossmodule-shadow.ll
new file mode 100644
index 0000000000000..5f4c7d541e5bd
--- /dev/null
+++ b/llvm/test/DebugInfo/NVPTX/intermediate-loc-crossmodule-shadow.ll
@@ -0,0 +1,78 @@
+; RUN: llc -O0 < %s -mtriple=nvptx64-nvidia-cuda \
+; RUN:   | FileCheck %s --implicit-check-not=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
+; RUN:                  --implicit-check-not=kernelA-snapshot-text
+;;
+;; Cross-module variant of the shadowing test: a
+;; separately-snapshotted kernelB is inlined POST-snapshot into a
+;; separately-snapshotted kernelA, so the two layer-bearing frames reference
+;; DIFFERENT intermediate DIFiles:
+;;
+;;   kernelB : 60  (head)  -- irlayers: B.tileir @ 200  (INNER, wins)
+;;     -> kernelA : 20     -- irlayers: A.tileir @ 50   (OUTER, shadowed)
+;;
+;; the NVPTX backend stops at the first layer-bearing frame (the head here),
+;; so B.tileir is recorded/emitted and A.tileir is never touched -- proving the
+;; shadowed frame's whole DIFile (its .file entry AND its source) is absent, not
+;; just its line. Only the inlined op is present, so A.tileir appears solely on
+;; the shadowed frame; a native kernelA op would legitimately emit A.tileir, but
+;; that is out of scope for isolating the shadow.
+
+;; Primary .loc = head (kernelB, line 60); intermediate = kernelB's B.tileir layer.
+; CHECK: .loc [[SRC:[0-9]+]] 60 5
+; CHECK-NEXT: .loc_intermediate [[BINT:[0-9]+]] 200 3
+;; Only one .loc_intermediate: the shadowed kernelA A.tileir layer is not emitted.
+; CHECK-NOT: .loc_intermediate
+
+; CHECK: .file [[SRC]] "/k{{/|\\\\}}kernel.py"
+;; Only B.tileir's checksum-named .file appears; A.tileir's (aaaa...) is shadowed
+;; out entirely (see the --implicit-check-not patterns on the RUN line).
+; CHECK: .file [[BINT]] ".{{/|\\\\}}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+
+;; One code_block, for B.tileir only.
+; CHECK: .nv_intermediate_source_section {
+; CHECK-NEXT: .code_block {
+; CHECK-NEXT: .ir_name: "tile ir"
+; CHECK-NEXT: .sourceFileName: [[BINT]]
+; CHECK-NEXT: .source_begin
+; CHECK-NEXT: kernelB-snapshot-text
+; CHECK-NEXT: .source_end
+; CHECK-NEXT: }
+; CHECK-NEXT: }
+
+define dso_local ptx_kernel void @test_kernel(ptr noundef %v) #0 !dbg !6 {
+entry:
+  store ptr null, ptr %v, align 8, !dbg !9
+  ret void, !dbg !8
+}
+
+attributes #0 = { noinline optnone "target-cpu"="sm_75" }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, producer: "tile", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "kernel.py", directory: "/k")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !DISubroutineType(types: !5)
+!5 = !{null}
+
+;; kernelA (the function) and kernelB (inlined into it post-snapshot).
+!6 = distinct !DISubprogram(name: "kernelA", scope: !1, file: !1, line: 10, type: !4, spFlags: DISPFlagDefinition, unit: !0)
+!7 = distinct !DISubprogram(name: "kernelB", scope: !1, file: !1, line: 40, type: !4, spFlags: DISPFlagDefinition, unit: !0)
+
+;; kernelB's OWN module snapshot (B.tileir) -- the INNER layer that wins.
+!14 = !DIFile(filename: "B.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", source: "kernelB-snapshot-text")
+!15 = !DILayerLoc(line: 200, column: 3, file: !14, kind: "tile ir")
+!16 = !DILayerLocList(!15)
+
+;; kernelA's OWN module snapshot (A.tileir) -- the OUTER layer, shadowed.
+!24 = !DIFile(filename: "A.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", source: "kernelA-snapshot-text")
+!25 = !DILayerLoc(line: 50, column: 1, file: !24, kind: "tile ir")
+!26 = !DILayerLocList(!25)
+
+;; Chain: kernelB op (head, B.tileir layer) inlinedAt kernelA call site
+;; (A.tileir layer, shadowed / outermost).
+!9 = !DILocation(line: 60, column: 5, scope: !7, inlinedAt: !10, irlayers: !16)
+!10 = !DILocation(line: 20, column: 1, scope: !6, irlayers: !26)
+!8 = !DILocation(line: 10, column: 1, scope: !6)
diff --git a/llvm/test/DebugInfo/NVPTX/intermediate-loc-dedup-gate.ll b/llvm/test/DebugInfo/NVPTX/intermediate-loc-dedup-gate.ll
new file mode 100644
index 0000000000000..96583c01fd442
--- /dev/null
+++ b/llvm/test/DebugInfo/NVPTX/intermediate-loc-dedup-gate.ll
@@ -0,0 +1,68 @@
+; RUN: llc -O0 < %s -mtriple=nvptx64-nvidia-cuda -mcpu=sm_70 -mattr=+ptx72 \
+; RUN:   | FileCheck %s
+
+;; Regression test for the DwarfDebug::beginInstruction dedup gate.
+;;
+;; DebugLoc::isSameSourceLocation used to compare only line/column/scope/
+;; inlinedAt. The intermediate coordinate lives on the
+;; DILocation's `irlayers` operand, so two instructions can share the same
+;; source coordinate (line 10, col 5, same scope) yet carry DIFFERENT layers.
+;; If isSameSourceLocation ignored irlayers, the two consecutive MachineInstrs
+;; would be deduped and the second .loc_intermediate silently dropped from the
+;; emitted PTX. isSameSourceLocation now also compares getRawIRLayers().
+;;
+;; This test pins both .loc_intermediate directives into the output so the
+;; dedup gate is forced to consider irlayers differences in its equivalence
+;; check.
+
+target triple = "nvptx64-nvidia-cuda"
+
+define i32 @dedup_gate_demo(i32 %a, i32 %b) !dbg !5 {
+  %1 = add i32 %a, %b, !dbg !20
+  %2 = mul i32 %1, 3,  !dbg !21
+  ret i32 %2,          !dbg !100
+}
+
+;; Both instructions share the primary source coordinate (line 10, col 5), but
+;; their `irlayers` differ (layer @ line 100 vs @ line 200). Because
+;; isSameSourceLocation now compares irlayers, the dedup gate does NOT fire:
+;; the primary .loc is re-emitted for the second instruction and each distinct
+;; .loc_intermediate is emitted in source order.
+
+; CHECK-LABEL: dedup_gate_demo
+;; First instruction (add): source + intermediate @ line 100.
+; CHECK:      .loc 1 10 5
+; CHECK-NEXT: .loc_intermediate {{[0-9]+}} 100 1
+;; Second instruction (mul): primary must be re-emitted (proves the dedup gate
+;; did NOT fire, because the irlayers operand differs) and the intermediate @
+;; line 200 must follow.
+; CHECK:      .loc 1 10 5
+; CHECK-NEXT: .loc_intermediate {{[0-9]+}} 200 1
+
+!llvm.dbg.cu                    = !{!2}
+!llvm.module.flags              = !{!0, !1}
+
+!0 = !{i32 2, !"Dwarf Version", i32 2}
+!1 = !{i32 2, !"Debug Info Version", i32 3}
+!2 = distinct !DICompileUnit(language: DW_LANG_C99, file: !3,
+                              emissionKind: DebugDirectivesOnly)
+!3 = !DIFile(filename: "demo.c", directory: "/tmp")
+!4 = !DISubroutineType(types: !{})
+!5 = distinct !DISubprogram(name: "dedup_gate_demo", scope: !3, file: !3,
+                            line: 1, type: !4, scopeLine: 1,
+                            spFlags: DISPFlagDefinition, unit: !2)
+
+!10 = !DIFile(filename: "demo.tile.ir", directory: "/tmp", checksumkind: CSK_MD5, checksum: "dddddddddddddddddddddddddddddddd", source: "demo tile ir text")
+
+;; Two distinct intermediate layers on the shared source coordinate.
+!110 = !DILayerLoc(line: 100, column: 1, file: !10, kind: "tile ir")
+!111 = !DILayerLoc(line: 200, column: 1, file: !10, kind: "tile ir")
+!120 = !DILayerLocList(!110)
+!121 = !DILayerLocList(!111)
+
+;; Same source (line 10, col 5, scope !5) but different layer lists.
+!20 = !DILocation(line: 10, column: 5, scope: !5, irlayers: !120)
+!21 = !DILocation(line: 10, column: 5, scope: !5, irlayers: !121)
+
+;; Bare source location (no layers).
+!100 = !DILocation(line: 10, column: 5, scope: !5)
diff --git a/llvm/test/DebugInfo/NVPTX/intermediate-loc-inline-shadow.ll b/llvm/test/DebugInfo/NVPTX/intermediate-loc-inline-shadow.ll
new file mode 100644
index 0000000000000..6deb80714e01c
--- /dev/null
+++ b/llvm/test/DebugInfo/NVPTX/intermediate-loc-inline-shadow.ll
@@ -0,0 +1,85 @@
+; RUN: llc -O0 < %s -mtriple=nvptx64-nvidia-cuda | FileCheck %s
+;;
+;; Two stacked layer-bearing frames in ONE inlined-at chain. The front end
+;; inlined funcB into funcA (snapshot on the funcA frame), and funcA was then
+;; inlined into kernelA -- which ALSO carried its own snapshot layer. So the
+;; store's chain is:
+;;
+;;   funcB : 52   (head)   -- no irlayers
+;;     -> funcA : 38       -- irlayers: tile ir @ 45   (INNER, wins)
+;;       -> kernelA : 15   -- irlayers: tile ir @ 12   (OUTER, shadowed)
+;;
+;; The producer disassembles the whole module into ONE frozen snapshot text, so both the
+;; funcA op (line 45) and the kernelA call op (line 12) index into the SAME
+;; intermediate DIFile -- they differ only by (line, col), not by file. The
+;; shadowing is therefore line-level: the NVPTX backend walks head -> outward
+;; to the FIRST (innermost) layer-bearing frame and stops, so the funcA line (45)
+;; is emitted and the kernelA line (12) never is. (Distinct intermediate DIFiles
+;; only arise across separately-snapshotted modules.)
+
+;; Primary .loc = the head (funcB, line 52); the winning intermediate layer is
+;; funcA's (line 45), read off a middle (non-head, non-outermost) frame.
+; CHECK: .loc [[SRC:[0-9]+]] 52 3
+; CHECK-NEXT: .loc_intermediate [[INT:[0-9]+]] 45 7
+;; Exactly one .loc_intermediate: the shadowed kernelA line (12) is not emitted.
+;; (A wrong outermost-frame walk would print `12 1` here; emitting both frames
+;; would add a second .loc_intermediate -- either way this fails.)
+; CHECK-NOT: .loc_intermediate
+
+; CHECK: .file [[SRC]] "/k{{/|\\\\}}kernel.py"
+;; A single intermediate .file (the shared module snapshot), named by its carried
+;; checksum digest (not MD5(filename)).
+; CHECK: .file [[INT]] ".{{/|\\\\}}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+
+;; One code_block for the one shared snapshot file.
+; CHECK: .nv_intermediate_source_section {
+; CHECK-NEXT: .code_block {
+; CHECK-NEXT: .ir_name: "tile ir"
+; CHECK-NEXT: .sourceFileName: [[INT]]
+; CHECK-NEXT: .source_begin
+; CHECK-NEXT: whole-module-tile-ir-snapshot
+; CHECK-NEXT: .source_end
+; CHECK-NEXT: }
+; CHECK-NEXT: }
+
+define dso_local ptx_kernel void @test_kernel(ptr noundef %v) #0 !dbg !6 {
+entry:
+  store ptr null, ptr %v, align 8, !dbg !9
+  ret void, !dbg !8
+}
+
+attributes #0 = { noinline optnone "target-cpu"="sm_75" }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, producer: "tile", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "kernel.py", directory: "/k")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !DISubroutineType(types: !5)
+!5 = !{null}
+
+;; Three source scopes: kernelA (the function), funcA, funcB.
+!6 = distinct !DISubprogram(name: "kernelA", scope: !1, file: !1, line: 10, type: !4, spFlags: DISPFlagDefinition, unit: !0)
+!7 = distinct !DISubprogram(name: "funcA", scope: !1, file: !1, line: 30, type: !4, spFlags: DISPFlagDefinition, unit: !0)
+!12 = distinct !DISubprogram(name: "funcB", scope: !1, file: !1, line: 50, type: !4, spFlags: DISPFlagDefinition, unit: !0)
+
+;; ONE whole-module tile-IR snapshot DIFile. Both frames' layers reference it,
+;; at different (line, col): funcA's op at 45:7, kernelA's call op at 12:1.
+!14 = !DIFile(filename: "module.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", source: "whole-module-tile-ir-snapshot")
+
+;; funcA's layer (the INNER layer that wins).
+!15 = !DILayerLoc(line: 45, column: 7, file: !14, kind: "tile ir")
+!16 = !DILayerLocList(!15)
+
+;; kernelA's layer (the OUTER layer that is SHADOWED) -- SAME file, different line.
+!25 = !DILayerLoc(line: 12, column: 1, file: !14, kind: "tile ir")
+!26 = !DILayerLocList(!25)
+
+;; The inlined-at chain: funcB head (no layer) -> funcA frame (layer 45) ->
+;; kernelA frame (layer 12, shadowed / outermost).
+!9 = !DILocation(line: 52, column: 3, scope: !12, inlinedAt: !10)
+!10 = !DILocation(line: 38, column: 7, scope: !7, inlinedAt: !11, irlayers: !16)
+!11 = !DILocation(line: 15, column: 1, scope: !6, irlayers: !26)
+!8 = !DILocation(line: 10, column: 1, scope: !6)
diff --git a/llvm/test/DebugInfo/NVPTX/intermediate-loc-multi-layer.ll b/llvm/test/DebugInfo/NVPTX/intermediate-loc-multi-layer.ll
new file mode 100644
index 0000000000000..03e352c65504c
--- /dev/null
+++ b/llvm/test/DebugInfo/NVPTX/intermediate-loc-multi-layer.ll
@@ -0,0 +1,62 @@
+; RUN: llc -O0 < %s -mtriple=nvptx64-nvidia-cuda | FileCheck %s
+;;
+;; A DILocation may carry more than one layer, one per intermediate IR level the
+;; program was lowered through. Check that each entry emits its own
+;; .loc_intermediate, in list order and right after the primary .loc, and that
+;; each contributes its own .code_block.
+
+; CHECK: .loc [[SRC:[0-9]+]] 2 5
+; CHECK-NEXT: .loc_intermediate [[TILE:[0-9]+]] 100 1
+; CHECK-NEXT: .loc_intermediate [[GPU:[0-9]+]] 7 3
+
+;; Each layer file gets its own secondary .file, named by its checksum digest.
+; CHECK-DAG: .file [[TILE]] ".{{/|\\\\}}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+; CHECK-DAG: .file [[GPU]] ".{{/|\\\\}}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+
+;; Both levels appear in the source section, in first-reference order.
+; CHECK: .nv_intermediate_source_section {
+; CHECK-NEXT: .code_block {
+; CHECK-NEXT: .ir_name: "tile ir"
+; CHECK-NEXT: .sourceFileName: [[TILE]]
+; CHECK-NEXT: .source_begin
+; CHECK-NEXT: tile ir text
+; CHECK-NEXT: .source_end
+; CHECK-NEXT: }
+; CHECK-NEXT: .code_block {
+; CHECK-NEXT: .ir_name: "gpu ir"
+; CHECK-NEXT: .sourceFileName: [[GPU]]
+; CHECK-NEXT: .source_begin
+; CHECK-NEXT: gpu ir text
+; CHECK-NEXT: .source_end
+; CHECK-NEXT: }
+; CHECK-NEXT: }
+
+define dso_local ptx_kernel void @test_kernel(ptr noundef %v) #0 !dbg !8 {
+entry:
+  %v.addr = alloca ptr, align 8
+  store ptr %v, ptr %v.addr, align 8, !dbg !20
+  ret void, !dbg !21
+}
+
+attributes #0 = { noinline optnone "target-cpu"="sm_75" }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cu", directory: "/test")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!8 = distinct !DISubprogram(name: "test_kernel", scope: !1, file: !1, line: 1, type: !9, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null}
+
+;; Two intermediate levels: tile IR lowered to GPU IR.
+!14 = !DIFile(filename: "kernel.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", source: "tile ir text")
+!15 = !DIFile(filename: "kernel.gpuir", directory: ".", checksumkind: CSK_MD5, checksum: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", source: "gpu ir text")
+!16 = !DILayerLoc(line: 100, column: 1, file: !14, kind: "tile ir")
+!17 = !DILayerLoc(line: 7, column: 3, file: !15, kind: "gpu ir")
+!18 = !DILayerLocList(!16, !17)
+
+!20 = !DILocation(line: 2, column: 5, scope: !8, irlayers: !18)
+!21 = !DILocation(line: 3, column: 1, scope: !8)
diff --git a/llvm/test/DebugInfo/NVPTX/intermediate-loc.ll b/llvm/test/DebugInfo/NVPTX/intermediate-loc.ll
new file mode 100644
index 0000000000000..1a155fc94dfdd
--- /dev/null
+++ b/llvm/test/DebugInfo/NVPTX/intermediate-loc.ll
@@ -0,0 +1,76 @@
+; RUN: llc -O0 < %s -mtriple=nvptx64-nvidia-cuda | FileCheck %s
+;; [TODO] Re-enable once ptxas changes have landed.
+;; RUN-TODO: %if ptxas %{ llc -O0 < %s -mtriple=nvptx64-nvidia-cuda | %ptxas-verify %}
+;;
+;; Test that DILocation.irlayers (multi-level line info) generates
+;; .loc_intermediate directives in PTX output: the primary source location is
+;; the DILocation, and each DILayerLoc entry emits a secondary directive.
+;; Instructions with only a source DILocation (no irlayers) do not emit one.
+
+; CHECK: .target sm_{{[0-9]+}}, debug
+
+; CHECK: .visible .func test_kernel
+
+;; First instruction (store ptr) is layered.
+; CHECK: .loc [[SRCFILE:[0-9]+]] 2 5
+; CHECK-NEXT: .loc_intermediate [[INTFILE:[0-9]+]] 100 10
+
+;; Second instruction (load ptr) has only a source DILocation.
+; CHECK: .loc [[SRCFILE]] 3 5
+; CHECK-NOT: .loc_intermediate
+
+;; Third instruction (load i32) is layered.
+; CHECK: .loc [[SRCFILE]] 5 5
+; CHECK-NEXT: .loc_intermediate [[INTFILE]] 100 10
+
+;; Fourth instruction (store i32) has only a source DILocation.
+; CHECK: .loc [[SRCFILE]] 6 5
+; CHECK-NOT: .loc_intermediate
+
+;; Fifth instruction (ret) is layered.
+; CHECK: .loc [[SRCFILE]] 4 1
+; CHECK-NEXT: .loc_intermediate [[INTFILE]] 100 10
+
+;; The .file declarations come after the function body.
+; CHECK: .file [[SRCFILE]] "/test{{/|\\\\}}test.cu"
+;; The secondary .file is named by the intermediate DIFile's checksum digest.
+; CHECK: .file [[INTFILE]] ".{{/|\\\\}}0123456789abcdef0123456789abcdef"
+
+define dso_local void @test_kernel(ptr noundef %v) #0 !dbg !8 {
+entry:
+  %v.addr = alloca ptr, align 8
+  %val = alloca i32, align 4
+  store ptr %v, ptr %v.addr, align 8, !dbg !20
+  %0 = load ptr, ptr %v.addr, align 8, !dbg !12
+  %1 = load i32, ptr %0, align 4, !dbg !23
+  store i32 %1, ptr %val, align 4, !dbg !19
+  ret void, !dbg !22
+}
+
+attributes #0 = { noinline optnone }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cu", directory: "/test")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!8 = distinct !DISubprogram(name: "test_kernel", scope: !1, file: !1, line: 1, type: !9, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null}
+
+;; Intermediate-IR layer: one DILayerLoc, shared (uniqued) across all layered
+;; instructions via a single DILayerLocList.
+!14 = !DIFile(filename: "intermediate.ptx", directory: ".", checksumkind: CSK_MD5, checksum: "0123456789abcdef0123456789abcdef", source: "intermediate ptx text")
+!30 = !DILayerLocList(!31)
+!31 = !DILayerLoc(line: 100, column: 10, file: !14, kind: "TileIR")
+
+;; Source-only instruction locations (no irlayers).
+!12 = !DILocation(line: 3, column: 5, scope: !8)
+!19 = !DILocation(line: 6, column: 5, scope: !8)
+
+;; Layered instruction locations: primary source loc + irlayers.
+!20 = !DILocation(line: 2, column: 5, scope: !8, irlayers: !30)
+!22 = !DILocation(line: 4, column: 1, scope: !8, irlayers: !30)
+!23 = !DILocation(line: 5, column: 5, scope: !8, irlayers: !30)
diff --git a/llvm/test/DebugInfo/NVPTX/intermediate-source-section-determinism.ll b/llvm/test/DebugInfo/NVPTX/intermediate-source-section-determinism.ll
new file mode 100644
index 0000000000000..4174a3163c241
--- /dev/null
+++ b/llvm/test/DebugInfo/NVPTX/intermediate-source-section-determinism.ll
@@ -0,0 +1,64 @@
+; RUN: llc -O0 < %s -mtriple=nvptx64-nvidia-cuda | FileCheck %s
+;;
+;; The .code_blocks in .nv_intermediate_source_section are emitted in a
+;; deterministic order: first-reference (insertion) order. The test pins that
+;; the ordering key is reference order, not the filename -- "bbb.tileir" is
+;; referenced first and "aaa.tileir" second, so bbb's code_block is emitted
+;; first even though "aaa" sorts earlier alphabetically.
+
+define dso_local ptx_kernel void @test_kernel(ptr noundef %v) #0 !dbg !8 {
+entry:
+  %v.addr = alloca ptr, align 8
+  store ptr %v, ptr %v.addr, align 8, !dbg !20
+  store ptr null, ptr %v.addr, align 8, !dbg !21
+  ret void, !dbg !22
+}
+
+attributes #0 = { noinline optnone "target-cpu"="sm_75" }
+
+;; bbb is referenced first, so it receives the lower .file number.
+; CHECK-DAG: .file [[FBBB:[0-9]+]] "{{.*}}22222222222222222222222222222222"
+; CHECK-DAG: .file [[FAAA:[0-9]+]] "{{.*}}11111111111111111111111111111111"
+
+; CHECK: .nv_intermediate_source_section {
+;; "bbb.tileir" is referenced first -> lower file number -> emitted first.
+; CHECK:      .code_block {
+; CHECK-NEXT:   .ir_name: "tile ir"
+; CHECK-NEXT:   .sourceFileName: [[FBBB]]
+; CHECK-NEXT:   .source_begin
+; CHECK-NEXT:   bbb source line
+; CHECK-NEXT:   .source_end
+;; "aaa.tileir" second (higher file number), despite sorting earlier by name.
+; CHECK:      .code_block {
+; CHECK-NEXT:   .ir_name: "tile ir"
+; CHECK-NEXT:   .sourceFileName: [[FAAA]]
+; CHECK-NEXT:   .source_begin
+; CHECK-NEXT:   aaa source line
+; CHECK-NEXT:   .source_end
+; CHECK: }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cu", directory: "/test")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!8 = distinct !DISubprogram(name: "test_kernel", scope: !1, file: !1, line: 1, type: !9, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null}
+
+;; aaa.tileir: sorts first by filename; carries a distinct checksum + source.
+!14 = !DIFile(filename: "aaa.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "11111111111111111111111111111111", source: "aaa source line")
+!15 = !DILayerLoc(line: 10, column: 1, file: !14, kind: "tile ir")
+!16 = !DILayerLocList(!15)
+
+;; bbb.tileir: sorts second by filename.
+!24 = !DIFile(filename: "bbb.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "22222222222222222222222222222222", source: "bbb source line")
+!25 = !DILayerLoc(line: 20, column: 1, file: !24, kind: "tile ir")
+!26 = !DILayerLocList(!25)
+
+;; First instruction references bbb, second references aaa (reverse of sort).
+!20 = !DILocation(line: 2, column: 5, scope: !8, irlayers: !26)
+!21 = !DILocation(line: 3, column: 5, scope: !8, irlayers: !16)
+!22 = !DILocation(line: 4, column: 1, scope: !8)
diff --git a/llvm/test/DebugInfo/NVPTX/intermediate-source-section-empty.ll b/llvm/test/DebugInfo/NVPTX/intermediate-source-section-empty.ll
new file mode 100644
index 0000000000000..361e08d7dd9cd
--- /dev/null
+++ b/llvm/test/DebugInfo/NVPTX/intermediate-source-section-empty.ll
@@ -0,0 +1,45 @@
+; RUN: llc -O0 < %s -mtriple=nvptx64-nvidia-cuda | FileCheck %s
+;;
+;; An instruction's DILayerLoc references an intermediate DIFile with NO source:
+;; text. A .loc_intermediate is only meaningful with the source it points into,
+;; so such a layer is dropped entirely rather than emitting a reference the
+;; consumer cannot resolve. With the only layer gone there is nothing to put in
+;; the source section either, so no section is emitted -- not even an empty stub.
+
+;; A non-empty PTX is still produced...
+; CHECK: .target sm_{{[0-9]+}}
+;; ... but the layer leaves no trace: no secondary location, no secondary .file,
+;; and no source section.
+; CHECK-NOT: .loc_intermediate
+; CHECK-NOT: cccccccccccccccccccccccccccccccc
+; CHECK-NOT: .nv_intermediate_source_section
+
+define dso_local void @no_intermediate_source(ptr noundef %v) #0 !dbg !8 {
+entry:
+  %v.addr = alloca ptr, align 8
+  store ptr %v, ptr %v.addr, align 8, !dbg !11
+  ret void, !dbg !12
+}
+
+attributes #0 = { noinline optnone "target-cpu"="sm_75" }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cu", directory: "/test")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!8 = distinct !DISubprogram(name: "no_intermediate_source", scope: !1, file: !1, line: 1, type: !9, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null}
+
+;; Intermediate file: checksum present but NO source: text, so no code_block is
+;; built for it.
+!4 = !DIFile(filename: "tileIR_source.unused", directory: ".", checksumkind: CSK_MD5, checksum: "cccccccccccccccccccccccccccccccc")
+!5 = !DILayerLoc(line: 100, column: 10, file: !4, kind: "TileIR")
+!6 = !DILayerLocList(!5)
+
+;; The store references the source-less intermediate layer; the ret is source-only.
+!11 = !DILocation(line: 2, column: 5, scope: !8, irlayers: !6)
+!12 = !DILocation(line: 4, column: 1, scope: !8)
diff --git a/llvm/test/DebugInfo/NVPTX/intermediate-source-section-mixed.ll b/llvm/test/DebugInfo/NVPTX/intermediate-source-section-mixed.ll
new file mode 100644
index 0000000000000..497819918fc7d
--- /dev/null
+++ b/llvm/test/DebugInfo/NVPTX/intermediate-source-section-mixed.ll
@@ -0,0 +1,62 @@
+; RUN: llc -O0 < %s -mtriple=nvptx64-nvidia-cuda | FileCheck %s
+;;
+;; Mixed intermediate files: one carries DIFile.source, the other only a checksum
+;; (no source). A .loc_intermediate is only meaningful with the source it points
+;; into -- the consumer rejects a reference to a file with no .code_block -- so
+;; the source-less layer is dropped ENTIRELY: no .loc_intermediate, no .file
+;; entry, no .code_block. The sourced layer is emitted as usual, named by its
+;; checksum digest. Contrast intermediate-source-section-empty.ll, where EVERY
+;; file is source-less and the section disappears with them.
+
+; CHECK: .loc_intermediate [[FA:[0-9]+]] 100 10
+; CHECK-NOT: .loc_intermediate
+; CHECK: .file [[FA]] ".{{/|\\\\}}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+; CHECK-NOT: .file
+
+;; The section holds EXACTLY ONE code_block -- the sourced file's. It closes
+;; right after it.
+; CHECK: .nv_intermediate_source_section {
+; CHECK-NEXT: .code_block {
+; CHECK-NEXT: .ir_name: "tile ir"
+; CHECK-NEXT: .sourceFileName: [[FA]]
+; CHECK-NEXT: .source_begin
+; CHECK-NEXT: aaa source line
+; CHECK-NEXT: .source_end
+; CHECK-NEXT: }
+; CHECK-NEXT: }
+
+define dso_local ptx_kernel void @test_kernel(ptr noundef %v) #0 !dbg !8 {
+entry:
+  %v.addr = alloca ptr, align 8
+  store ptr %v, ptr %v.addr, align 8, !dbg !20
+  store ptr null, ptr %v.addr, align 8, !dbg !21
+  ret void, !dbg !22
+}
+
+attributes #0 = { noinline optnone "target-cpu"="sm_75" }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cu", directory: "/test")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!8 = distinct !DISubprogram(name: "test_kernel", scope: !1, file: !1, line: 1, type: !9, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null}
+
+;; Sourced intermediate file (referenced first -> lower .file number).
+!14 = !DIFile(filename: "aaa.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", source: "aaa source line")
+!15 = !DILayerLoc(line: 100, column: 10, file: !14, kind: "tile ir")
+!16 = !DILayerLocList(!15)
+
+;; Source-less intermediate file (checksum only, no source:).
+!24 = !DIFile(filename: "bbb.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
+!25 = !DILayerLoc(line: 200, column: 20, file: !24, kind: "tile ir")
+!26 = !DILayerLocList(!25)
+
+;; First instruction references the sourced file; second the source-less file.
+!20 = !DILocation(line: 2, column: 5, scope: !8, irlayers: !16)
+!21 = !DILocation(line: 3, column: 5, scope: !8, irlayers: !26)
+!22 = !DILocation(line: 4, column: 1, scope: !8)
diff --git a/llvm/test/DebugInfo/NVPTX/intermediate-source-section-no-checksum.ll b/llvm/test/DebugInfo/NVPTX/intermediate-source-section-no-checksum.ll
new file mode 100644
index 0000000000000..7600cce432c87
--- /dev/null
+++ b/llvm/test/DebugInfo/NVPTX/intermediate-source-section-no-checksum.ll
@@ -0,0 +1,51 @@
+; RUN: llc -O0 < %s -mtriple=nvptx64-nvidia-cuda | FileCheck %s
+;;
+;; An intermediate file that carries source but NO checksum still needs a unique
+;; hash for its secondary .file name -- ptxas stores the .code_block's .source in
+;; the cubin keyed by that name -- so emission hashes the DIFile's directory and
+;; filename instead of using the path verbatim. Contrast
+;; intermediate-checksum-secondary-file.ll, where a checksum is present and that
+;; content-addressed digest is used instead.
+
+;; MD5(".kernel.tileir") == directory "." concatenated with filename.
+; CHECK-DAG: .loc_intermediate [[F:[0-9]+]] 42 5
+; CHECK-DAG: .file [[F]] ".{{/|\\\\}}f3c6d19eaf8d63898bcec70cb38e2482"
+
+;; ...and the source still reaches the section.
+; CHECK: .nv_intermediate_source_section {
+; CHECK-NEXT: .code_block {
+; CHECK-NEXT: .ir_name: "tile ir"
+; CHECK-NEXT: .sourceFileName: [[F]]
+; CHECK-NEXT: .source_begin
+; CHECK-NEXT: tile ir source
+; CHECK-NEXT: .source_end
+; CHECK-NEXT: }
+; CHECK-NEXT: }
+
+define dso_local ptx_kernel void @test_kernel(ptr noundef %v) #0 !dbg !8 {
+entry:
+  %v.addr = alloca ptr, align 8
+  store ptr %v, ptr %v.addr, align 8, !dbg !20
+  ret void, !dbg !21
+}
+
+attributes #0 = { noinline optnone "target-cpu"="sm_75" }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cu", directory: "/test")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!8 = distinct !DISubprogram(name: "test_kernel", scope: !1, file: !1, line: 1, type: !9, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null}
+
+;; Source present, checksum absent.
+!14 = !DIFile(filename: "kernel.tileir", directory: ".", source: "tile ir source")
+!30 = !DILayerLocList(!31)
+!31 = !DILayerLoc(line: 42, column: 5, file: !14, kind: "tile ir")
+
+!20 = !DILocation(line: 2, column: 5, scope: !8, irlayers: !30)
+!21 = !DILocation(line: 3, column: 1, scope: !8)
diff --git a/llvm/test/DebugInfo/NVPTX/intermediate-source-section-shared-filenum.ll b/llvm/test/DebugInfo/NVPTX/intermediate-source-section-shared-filenum.ll
new file mode 100644
index 0000000000000..4b1b5eb31e4dc
--- /dev/null
+++ b/llvm/test/DebugInfo/NVPTX/intermediate-source-section-shared-filenum.ll
@@ -0,0 +1,58 @@
+; RUN: llc -O0 < %s -mtriple=nvptx64-nvidia-cuda | FileCheck %s
+;;
+;; Two intermediate DIFiles with identical content under different filenames
+;; collapse onto one emitted .file number (a sourced file is named by its
+;; checksum digest). Check the source section emits exactly one .code_block
+;; for that number.
+
+;; Both layers resolve to the same .file number, so both .loc_intermediate
+;; directives reference it, and only one .file directive is emitted for it.
+; CHECK-DAG: .loc_intermediate [[F:[0-9]+]] 100 10
+; CHECK-DAG: .loc_intermediate [[F]] 200 20
+; CHECK-DAG: .file [[F]] ".{{/|\\\\}}cccccccccccccccccccccccccccccccc"
+
+;; EXACTLY ONE code_block: the section closes immediately after it.
+; CHECK: .nv_intermediate_source_section {
+; CHECK-NEXT: .code_block {
+; CHECK-NEXT: .ir_name: "tile ir"
+; CHECK-NEXT: .sourceFileName: [[F]]
+; CHECK-NEXT: .source_begin
+; CHECK-NEXT: shared source line
+; CHECK-NEXT: .source_end
+; CHECK-NEXT: }
+; CHECK-NEXT: }
+
+define dso_local ptx_kernel void @test_kernel(ptr noundef %v) #0 !dbg !8 {
+entry:
+  %v.addr = alloca ptr, align 8
+  store ptr %v, ptr %v.addr, align 8, !dbg !20
+  store ptr null, ptr %v.addr, align 8, !dbg !21
+  ret void, !dbg !22
+}
+
+attributes #0 = { noinline optnone "target-cpu"="sm_75" }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cu", directory: "/test")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!8 = distinct !DISubprogram(name: "test_kernel", scope: !1, file: !1, line: 1, type: !9, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null}
+
+;; Two DIFile nodes differing ONLY in filename -- same directory, same checksum,
+;; same source -- so they are distinct metadata but name the same content.
+!14 = !DIFile(filename: "a.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "cccccccccccccccccccccccccccccccc", source: "shared source line")
+!15 = !DILayerLoc(line: 100, column: 10, file: !14, kind: "tile ir")
+!16 = !DILayerLocList(!15)
+
+!24 = !DIFile(filename: "b.tileir", directory: ".", checksumkind: CSK_MD5, checksum: "cccccccccccccccccccccccccccccccc", source: "shared source line")
+!25 = !DILayerLoc(line: 200, column: 20, file: !24, kind: "tile ir")
+!26 = !DILayerLocList(!25)
+
+!20 = !DILocation(line: 2, column: 5, scope: !8, irlayers: !16)
+!21 = !DILocation(line: 3, column: 5, scope: !8, irlayers: !26)
+!22 = !DILocation(line: 4, column: 1, scope: !8)
diff --git a/llvm/test/DebugInfo/NVPTX/intermediate-source-section.ll b/llvm/test/DebugInfo/NVPTX/intermediate-source-section.ll
new file mode 100644
index 0000000000000..4a657801f0b1c
--- /dev/null
+++ b/llvm/test/DebugInfo/NVPTX/intermediate-source-section.ll
@@ -0,0 +1,81 @@
+; RUN: llc -O0 < %s -mtriple=nvptx64-nvidia-cuda | FileCheck %s
+;; TODO: Enable once PTXAS changes land.
+;; RUN-TODO: %if ptxas %{ llc -O0 < %s -mtriple=nvptx64-nvidia-cuda | %ptxas-verify %}
+;;
+;; Test that intermediate DIFile.source text generates the
+;; .nv_intermediate_source_section in PTX output. This section contains the
+;; high-level source code from intermediate representations like TileIR.
+;;
+;; The layers live on the DILocation's `irlayers` operand: each
+;; instruction's DILocation carries a DILayerLocList of DILayerLoc entries that
+;; reference the intermediate DIFile. The code_block for a declared source file
+;; is emitted only when some instruction's layer references that file (see
+;; intermediate-source-section-empty.ll for the skip case).
+
+;; Check that .file directives are emitted for the intermediate source files.
+;; The secondary .file name is the intermediate DIFile's carried checksum digest.
+; CHECK-DAG: .file [[FILE123:[0-9]+]] "{{.*}}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+; CHECK-DAG: .file [[FILE456:[0-9]+]] "{{.*}}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+
+;; Check that the intermediate source section is emitted
+; CHECK: .nv_intermediate_source_section {
+
+;; First code block with TileIR - sourceFileName matches the file number
+; CHECK:   .code_block {
+; CHECK-NEXT:     .ir_name: "TileIR"
+; CHECK-NEXT:     .sourceFileName: [[FILE123]]
+; CHECK-NEXT:     .source_begin
+; CHECK-NEXT:     %0 = memref.load %arg0[] : memref<f32>
+; CHECK-NEXT:     .source_end
+
+;; Second code block with TileIR - sourceFileName matches the file number
+; CHECK:   .code_block {
+; CHECK-NEXT:     .ir_name: "TileIR"
+; CHECK-NEXT:     .sourceFileName: [[FILE456]]
+; CHECK-NEXT:     .source_begin
+; CHECK-NEXT:     memref.store %0, %arg1[] : memref<f32>
+; CHECK-NEXT:     .source_end
+
+;; Close the second code_block, then the section itself.
+; CHECK-NEXT: }
+; CHECK-NEXT: }
+
+define dso_local ptx_kernel void @test_kernel(ptr noundef %v) #0 !dbg !8 {
+entry:
+  %v.addr = alloca ptr, align 8
+  store ptr %v, ptr %v.addr, align 8, !dbg !20
+  %0 = load ptr, ptr %v.addr, align 8, !dbg !21
+  store ptr %0, ptr %v.addr, align 8, !dbg !22
+  ret void, !dbg !23
+}
+
+attributes #0 = { noinline optnone "target-cpu"="sm_75" }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+!1 = !DIFile(filename: "test.cu", directory: "/test")
+!2 = !{i32 7, !"Dwarf Version", i32 2}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!8 = distinct !DISubprogram(name: "test_kernel", scope: !1, file: !1, line: 1, type: !9, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0)
+!9 = !DISubroutineType(types: !10)
+!10 = !{null}
+
+;; High-level source file reference for tileIR_source.123, with its shared layer.
+!15 = !DIFile(filename: "tileIR_source.123", directory: ".", checksumkind: CSK_MD5, checksum: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", source: "%0 = memref.load %arg0[] : memref<f32>")
+!17 = !DILayerLoc(line: 100, column: 10, file: !15, kind: "TileIR")
+!18 = !DILayerLocList(!17)
+
+;; High-level source file reference for tileIR_source.456, with its shared layer.
+!24 = !DIFile(filename: "tileIR_source.456", directory: ".", checksumkind: CSK_MD5, checksum: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", source: "memref.store %0, %arg1[] : memref<f32>")
+!26 = !DILayerLoc(line: 200, column: 15, file: !24, kind: "TileIR")
+!27 = !DILayerLocList(!26)
+
+;; Instruction locations: source DILocation + irlayers.
+;; First two instructions reference tileIR_source.123
+!20 = !DILocation(line: 2, column: 5, scope: !8, irlayers: !18)
+!21 = !DILocation(line: 3, column: 5, scope: !8, irlayers: !18)
+;; Last two instructions reference tileIR_source.456
+!22 = !DILocation(line: 4, column: 5, scope: !8, irlayers: !27)
+!23 = !DILocation(line: 5, column: 1, scope: !8, irlayers: !27)



More information about the llvm-commits mailing list