https://github.com/RiverDave updated 
https://github.com/llvm/llvm-project/pull/206576

>From be1086b6453cc16402913c60f145a39abe7b3630 Mon Sep 17 00:00:00 2001
From: David Rivera <[email protected]>
Date: Mon, 29 Jun 2026 15:46:11 -0400
Subject: [PATCH 1/5] [CIR] Add offload container operation

Introduce cir.offload.container, a CIR dialect operation that groups one host 
CIR module with one or more device CIR modules in a single IR unit.

Nested modules are tagged with the new cir.offload.kind enum attribute using 
#cir.offload_kind<host> and #cir.offload_kind<device>. The verifier enforces 
the structural contract expected by follow-up offload merge/split pipeline 
patches: host module first, device modules after it, only nested builtin.module 
ops, and at least one device module.

This patch only adds the IR representation and verifier tests; the passes that 
create, consume, or split the container are left to later patches.
---
 .../clang/CIR/Dialect/IR/CIRDialect.td        |  1 +
 clang/include/clang/CIR/Dialect/IR/CIROps.td  | 59 ++++++++++++++++++
 clang/lib/CIR/Dialect/IR/CIRDialect.cpp       | 61 +++++++++++++++++++
 .../test/CIR/IR/invalid-offload-container.cir | 54 ++++++++++++++++
 clang/test/CIR/IR/offload-container.cir       | 32 ++++++++++
 5 files changed, 207 insertions(+)
 create mode 100644 clang/test/CIR/IR/invalid-offload-container.cir
 create mode 100644 clang/test/CIR/IR/offload-container.cir

diff --git a/clang/include/clang/CIR/Dialect/IR/CIRDialect.td 
b/clang/include/clang/CIR/Dialect/IR/CIRDialect.td
index 9ea186489ac65..00c8cfe008408 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRDialect.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIRDialect.td
@@ -53,6 +53,7 @@ def CIR_Dialect : Dialect {
     static llvm::StringRef getModuleLevelAsmAttrName() { return 
"cir.module_asm"; }
     static llvm::StringRef getGlobalCtorsAttrName() { return 
"cir.global_ctors"; }
     static llvm::StringRef getGlobalDtorsAttrName() { return 
"cir.global_dtors"; }
+    static llvm::StringRef getOffloadKindAttrName() { return 
"cir.offload.kind"; }
     static llvm::StringRef getOperandSegmentSizesAttrName() { return 
"operandSegmentSizes"; }
     static llvm::StringRef getNoCallerSavedRegsAttrName() { return 
"no_caller_saved_registers"; }
     static llvm::StringRef getNoCallbackAttrName() { return "nocallback"; }
diff --git a/clang/include/clang/CIR/Dialect/IR/CIROps.td 
b/clang/include/clang/CIR/Dialect/IR/CIROps.td
index 6d149e6b1ccaa..158b97adfbe4e 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIROps.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td
@@ -6141,6 +6141,65 @@ def CIR_VecSplatOp : CIR_Op<"vec.splat", [
   }];
 }
 
+//===----------------------------------------------------------------------===//
+// OffloadKind
+//===----------------------------------------------------------------------===//
+
+def CIR_OffloadKind : CIR_I32EnumAttr<"OffloadKind", "offload kind", [
+  I32EnumAttrCase<"Host", 0, "host">,
+  I32EnumAttrCase<"Device", 1, "device">
+]> {
+  let genSpecializedAttr = 0;
+}
+
+def CIR_OffloadKindAttr : CIR_EnumAttr<CIR_OffloadKind, "offload_kind"> {
+  let summary = "Offload kind (host or device)";
+}
+
+//===----------------------------------------------------------------------===//
+// OffloadContainerOp
+//===----------------------------------------------------------------------===//
+
+def CIR_OffloadContainerOp : CIR_Op<"offload.container", [
+    NoRegionArguments, NoTerminator, SingleBlock, SymbolTable]> {
+  let summary = "Container for host and device CIR modules";
+  let description = [{
+    `cir.offload.container` groups one host CIR module with one or more device
+    CIR modules for offload-aware analysis and transformation.
+
+    The body holds nested `builtin.module` operations. The first nested
+    module is the host module and must carry
+    `cir.offload.kind = #cir.offload_kind<host>`. All remaining nested
+    modules are device modules and must carry
+    `cir.offload.kind = #cir.offload_kind<device>`. There must be at least
+    one device module.
+
+    Example:
+
+    ```mlir
+    cir.offload.container {
+      builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
+      }
+      builtin.module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
+      }
+    }
+    ```
+  }];
+
+  let regions = (region SizedRegion<1>:$body);
+
+  let assemblyFormat = "$body attr-dict";
+
+  let hasVerifier = 1;
+  let hasLLVMLowering = false;
+
+  let extraClassDeclaration = [{
+    mlir::ModuleOp getHostModule();
+    llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
+    getDeviceModules();
+  }];
+}
+
 
//===----------------------------------------------------------------------===//
 // BaseClassAddrOp
 
//===----------------------------------------------------------------------===//
diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp 
b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index 606b4fd9d3fa6..b36424aa31ded 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -18,6 +18,7 @@
 
 #include "mlir/IR/Attributes.h"
 #include "mlir/IR/BuiltinTypes.h"
+#include "mlir/IR/BuiltinOps.h"
 #include "mlir/IR/DialectImplementation.h"
 #include "mlir/IR/PatternMatch.h"
 #include "mlir/IR/Value.h"
@@ -2374,6 +2375,66 @@ LogicalResult cir::VTTAddrPointOp::verify() {
   return success();
 }
 
+//===----------------------------------------------------------------------===//
+// OffloadContainerOp
+//===----------------------------------------------------------------------===//
+
+mlir::ModuleOp cir::OffloadContainerOp::getHostModule() {
+  return mlir::cast<mlir::ModuleOp>(getBody().front().front());
+}
+
+llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
+cir::OffloadContainerOp::getDeviceModules() {
+  mlir::Block &body = getBody().front();
+  auto begin = body.op_begin<mlir::ModuleOp>();
+  auto end = body.op_end<mlir::ModuleOp>();
+  if (begin != end)
+    ++begin;
+  return {begin, end};
+}
+
+static LogicalResult checkOffloadKind(mlir::ModuleOp module,
+                                      cir::OffloadKind expected) {
+  auto attr = module->getAttrOfType<cir::OffloadKindAttr>(
+      cir::CIRDialect::getOffloadKindAttrName());
+  if (!attr)
+    return module.emitOpError()
+           << "expects '" << cir::CIRDialect::getOffloadKindAttrName()
+           << "' offload kind attribute";
+  if (attr.getValue() != expected)
+    return module.emitOpError()
+           << "expects '" << cir::CIRDialect::getOffloadKindAttrName()
+           << "' value '" << cir::stringifyOffloadKind(expected) << "'";
+  return success();
+}
+
+LogicalResult cir::OffloadContainerOp::verify() {
+  mlir::Block &body = getBody().front();
+  if (body.empty())
+    return emitOpError() << "expects host module as the first nested op";
+
+  auto host = mlir::dyn_cast<mlir::ModuleOp>(body.front());
+  if (!host)
+    return emitOpError() << "expects host module as the first nested op";
+  if (failed(checkOffloadKind(host, cir::OffloadKind::Host)))
+    return failure();
+
+  unsigned numDevices = 0;
+  auto it = body.begin();
+  for (++it; it != body.end(); ++it) {
+    auto module = mlir::dyn_cast<mlir::ModuleOp>(*it);
+    if (!module)
+      return emitOpError() << "expects only nested builtin.module ops";
+    if (failed(checkOffloadKind(module, cir::OffloadKind::Device)))
+      return failure();
+    ++numDevices;
+  }
+
+  if (numDevices == 0)
+    return emitOpError() << "expects at least one device module";
+  return success();
+}
+
 
//===----------------------------------------------------------------------===//
 // FuncOp
 
//===----------------------------------------------------------------------===//
diff --git a/clang/test/CIR/IR/invalid-offload-container.cir 
b/clang/test/CIR/IR/invalid-offload-container.cir
new file mode 100644
index 0000000000000..219442f39674a
--- /dev/null
+++ b/clang/test/CIR/IR/invalid-offload-container.cir
@@ -0,0 +1,54 @@
+// RUN: cir-opt %s -verify-diagnostics -split-input-file
+
+module {
+  cir.offload.container {
+    builtin.module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} { // expected-error {{expects 'cir.offload.kind' 
value 'host'}}
+    }
+  }
+}
+
+// -----
+
+module {
+  cir.offload.container {
+    builtin.module @host_0 attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
+    }
+    builtin.module @host_1 attributes {cir.offload.kind = 
#cir.offload_kind<host>} { // expected-error {{expects 'cir.offload.kind' value 
'device'}}
+    }
+  }
+}
+
+// -----
+
+module {
+  cir.offload.container {
+    builtin.module @host { // expected-error {{expects 'cir.offload.kind' 
offload kind attribute}}
+    }
+  }
+}
+
+// -----
+
+module {
+  cir.offload.container { // expected-error {{expects only nested 
builtin.module ops}}
+    builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
+    }
+    cir.const #cir.int<0> : !cir.int<s, 32>
+  }
+}
+
+// -----
+
+module {
+  cir.offload.container { // expected-error {{expects at least one device 
module}}
+    builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
+    }
+  }
+}
+
+// -----
+
+module {
+  cir.offload.container { // expected-error {{expects host module as the first 
nested op}}
+  }
+}
diff --git a/clang/test/CIR/IR/offload-container.cir 
b/clang/test/CIR/IR/offload-container.cir
new file mode 100644
index 0000000000000..b7b53aa973ce5
--- /dev/null
+++ b/clang/test/CIR/IR/offload-container.cir
@@ -0,0 +1,32 @@
+// RUN: cir-opt %s -split-input-file --verify-roundtrip | FileCheck %s
+
+module {
+  cir.offload.container {
+    builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
+    }
+    builtin.module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
+    }
+  }
+}
+
+// CHECK: cir.offload.container {
+// CHECK:   builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
+// CHECK:   builtin.module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
+
+// -----
+
+module {
+  cir.offload.container {
+    builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
+    }
+    builtin.module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
+    }
+    builtin.module @device_1 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
+    }
+  }
+}
+
+// CHECK: cir.offload.container {
+// CHECK:   builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
+// CHECK:   builtin.module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
+// CHECK:   builtin.module @device_1 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {

>From c728fb4c32ae57382a1f8903bed2a6ef1b20ff8d Mon Sep 17 00:00:00 2001
From: David Rivera <[email protected]>
Date: Mon, 29 Jun 2026 16:00:27 -0400
Subject: [PATCH 2/5] fix fmt

---
 clang/lib/CIR/Dialect/IR/CIRDialect.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp 
b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index b36424aa31ded..5415e9285801d 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -17,8 +17,8 @@
 #include "clang/CIR/Dialect/IR/CIRTypes.h"
 
 #include "mlir/IR/Attributes.h"
-#include "mlir/IR/BuiltinTypes.h"
 #include "mlir/IR/BuiltinOps.h"
+#include "mlir/IR/BuiltinTypes.h"
 #include "mlir/IR/DialectImplementation.h"
 #include "mlir/IR/PatternMatch.h"
 #include "mlir/IR/Value.h"

>From 4054988194f0af15917f36936cbd6f686b8dcaf4 Mon Sep 17 00:00:00 2001
From: David Rivera <[email protected]>
Date: Fri, 14 Aug 2026 08:11:59 -0400
Subject: [PATCH 3/5] [CIR] Drop `cir.offload.container` and represent offload
 modules through a unit market on MLIR modules.

---
 .../include/clang/CIR/Dialect/IR/CIRAttrs.td  |  42 +++++
 .../include/clang/CIR/Dialect/IR/CIRDialect.h |  13 ++
 .../clang/CIR/Dialect/IR/CIRDialect.td        |   3 +
 clang/include/clang/CIR/Dialect/IR/CIROps.td  |  59 -------
 clang/lib/CIR/Dialect/IR/CIRDialect.cpp       | 166 +++++++++++-------
 .../test/CIR/IR/invalid-offload-container.cir |  70 +++++---
 clang/test/CIR/IR/offload-container.cir       |  38 ++--
 7 files changed, 229 insertions(+), 162 deletions(-)

diff --git a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td 
b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
index 838d8975177fd..691b0c311389d 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
@@ -115,6 +115,48 @@ def CIR_SourceLanguageAttr : 
CIR_EnumAttr<CIR_SourceLanguage, "lang"> {
   }];
 }
 
+//===----------------------------------------------------------------------===//
+// OffloadKindAttr
+//===----------------------------------------------------------------------===//
+
+def CIR_OffloadKind : CIR_I32EnumAttr<"OffloadKind", "offload kind", [
+  I32EnumAttrCase<"Host", 0, "host">,
+  I32EnumAttrCase<"Device", 1, "device">
+]> {
+  let genSpecializedAttr = 0;
+}
+
+def CIR_OffloadKindAttr : CIR_EnumAttr<CIR_OffloadKind, "offload_kind"> {
+  let summary = "Offload kind of a module in an offload container";
+  let description = [{
+    Marks a module as either the host module or one of the device modules of
+    an offload container, keeping a host CIR module and its associated device
+    CIR modules in one IR unit while offload merge/split pipeline passes need
+    visibility into both sides.
+
+    A module carrying the `cir.offload.container` unit attribute is such a
+    container. Its body holds only nested modules: the first one is the host
+    module and must carry `cir.offload.kind = #cir.offload_kind<host>`, and
+    all remaining ones are device modules and must carry
+    `cir.offload.kind = #cir.offload_kind<device>`. There must be at least one
+    device module. Keeping the host module first gives later passes a simple
+    convention for finding the host side while iterating the remaining device
+    modules.
+
+    Example:
+    ```mlir
+    module attributes {cir.offload.container} {
+      module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {}
+      module @device attributes {cir.offload.kind = #cir.offload_kind<device>} 
{}
+    }
+    ```
+
+    The attribute names `cir.offload.container` and `cir.offload.kind` are
+    defined by the `getOffloadContainerAttrName` and `getOffloadKindAttrName`
+    methods in the CIRDialect class.
+  }];
+}
+
 
//===----------------------------------------------------------------------===//
 // ArgPassingKind + RecordLayoutAttr
 
//===----------------------------------------------------------------------===//
diff --git a/clang/include/clang/CIR/Dialect/IR/CIRDialect.h 
b/clang/include/clang/CIR/Dialect/IR/CIRDialect.h
index 2f1ef5b6cb9e0..c61bd414036cb 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRDialect.h
+++ b/clang/include/clang/CIR/Dialect/IR/CIRDialect.h
@@ -88,6 +88,19 @@ RecordLayoutAttr getRecordLayout(mlir::ModuleOp mod, 
mlir::StringAttr name);
 /// Same lookup as getRecordLayout, but returns a null attribute instead of
 /// asserting when the record has no layout entry.
 RecordLayoutAttr tryGetRecordLayout(mlir::ModuleOp mod, mlir::StringAttr name);
+RecordLayoutAttr getRecordLayout(mlir::ModuleOp module, mlir::StringAttr name);
+
+/// Returns whether the module is an offload container, i.e. whether it carries
+/// the cir.offload.container unit attribute.  See CIR_OffloadKindAttr for the
+/// structure such a module is required to have.
+bool isOffloadContainer(mlir::ModuleOp module);
+
+/// Returns the host module of an offload container.
+mlir::ModuleOp getOffloadHostModule(mlir::ModuleOp container);
+
+/// Returns the device modules of an offload container, in container order.
+llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
+getOffloadDeviceModules(mlir::ModuleOp container);
 } // namespace cir
 
 // TableGen'erated files for MLIR dialects require that a macro be defined when
diff --git a/clang/include/clang/CIR/Dialect/IR/CIRDialect.td 
b/clang/include/clang/CIR/Dialect/IR/CIRDialect.td
index 00c8cfe008408..7c5c2d20bbaf4 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRDialect.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIRDialect.td
@@ -26,6 +26,8 @@ def CIR_Dialect : Dialect {
 
   let useDefaultAttributePrinterParser = 1;
 
+  let hasOperationAttrVerify = 1;
+
   // Enable constant materialization for the CIR dialect. This generates a
   // declaration for the cir::CIRDialect::materializeConstant function. This
   // hook is necessary for canonicalization to properly handle attributes
@@ -53,6 +55,7 @@ def CIR_Dialect : Dialect {
     static llvm::StringRef getModuleLevelAsmAttrName() { return 
"cir.module_asm"; }
     static llvm::StringRef getGlobalCtorsAttrName() { return 
"cir.global_ctors"; }
     static llvm::StringRef getGlobalDtorsAttrName() { return 
"cir.global_dtors"; }
+    static llvm::StringRef getOffloadContainerAttrName() { return 
"cir.offload.container"; }
     static llvm::StringRef getOffloadKindAttrName() { return 
"cir.offload.kind"; }
     static llvm::StringRef getOperandSegmentSizesAttrName() { return 
"operandSegmentSizes"; }
     static llvm::StringRef getNoCallerSavedRegsAttrName() { return 
"no_caller_saved_registers"; }
diff --git a/clang/include/clang/CIR/Dialect/IR/CIROps.td 
b/clang/include/clang/CIR/Dialect/IR/CIROps.td
index 158b97adfbe4e..6d149e6b1ccaa 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIROps.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td
@@ -6141,65 +6141,6 @@ def CIR_VecSplatOp : CIR_Op<"vec.splat", [
   }];
 }
 
-//===----------------------------------------------------------------------===//
-// OffloadKind
-//===----------------------------------------------------------------------===//
-
-def CIR_OffloadKind : CIR_I32EnumAttr<"OffloadKind", "offload kind", [
-  I32EnumAttrCase<"Host", 0, "host">,
-  I32EnumAttrCase<"Device", 1, "device">
-]> {
-  let genSpecializedAttr = 0;
-}
-
-def CIR_OffloadKindAttr : CIR_EnumAttr<CIR_OffloadKind, "offload_kind"> {
-  let summary = "Offload kind (host or device)";
-}
-
-//===----------------------------------------------------------------------===//
-// OffloadContainerOp
-//===----------------------------------------------------------------------===//
-
-def CIR_OffloadContainerOp : CIR_Op<"offload.container", [
-    NoRegionArguments, NoTerminator, SingleBlock, SymbolTable]> {
-  let summary = "Container for host and device CIR modules";
-  let description = [{
-    `cir.offload.container` groups one host CIR module with one or more device
-    CIR modules for offload-aware analysis and transformation.
-
-    The body holds nested `builtin.module` operations. The first nested
-    module is the host module and must carry
-    `cir.offload.kind = #cir.offload_kind<host>`. All remaining nested
-    modules are device modules and must carry
-    `cir.offload.kind = #cir.offload_kind<device>`. There must be at least
-    one device module.
-
-    Example:
-
-    ```mlir
-    cir.offload.container {
-      builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
-      }
-      builtin.module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
-      }
-    }
-    ```
-  }];
-
-  let regions = (region SizedRegion<1>:$body);
-
-  let assemblyFormat = "$body attr-dict";
-
-  let hasVerifier = 1;
-  let hasLLVMLowering = false;
-
-  let extraClassDeclaration = [{
-    mlir::ModuleOp getHostModule();
-    llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
-    getDeviceModules();
-  }];
-}
-
 
//===----------------------------------------------------------------------===//
 // BaseClassAddrOp
 
//===----------------------------------------------------------------------===//
diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp 
b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index 5415e9285801d..45e3f011e6269 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -111,6 +111,112 @@ Operation 
*cir::CIRDialect::materializeConstant(mlir::OpBuilder &builder,
                                  mlir::cast<mlir::TypedAttr>(value));
 }
 
+//===----------------------------------------------------------------------===//
+// Offload container helpers
+//===----------------------------------------------------------------------===//
+
+bool cir::isOffloadContainer(mlir::ModuleOp module) {
+  return module->hasAttr(cir::CIRDialect::getOffloadContainerAttrName());
+}
+
+mlir::ModuleOp cir::getOffloadHostModule(mlir::ModuleOp container) {
+  assert(isOffloadContainer(container) && "expected an offload container");
+  return mlir::cast<mlir::ModuleOp>(container.getBody()->front());
+}
+
+llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
+cir::getOffloadDeviceModules(mlir::ModuleOp container) {
+  assert(isOffloadContainer(container) && "expected an offload container");
+  mlir::Block &body = *container.getBody();
+  auto begin = body.op_begin<mlir::ModuleOp>();
+  auto end = body.op_end<mlir::ModuleOp>();
+  if (begin != end)
+    ++begin;
+  return {begin, end};
+}
+
+//===----------------------------------------------------------------------===//
+// Dialect attribute verification
+//===----------------------------------------------------------------------===//
+
+static LogicalResult verifyOffloadKind(mlir::ModuleOp module,
+                                       cir::OffloadKind expected) {
+  auto attr = module->getAttrOfType<cir::OffloadKindAttr>(
+      cir::CIRDialect::getOffloadKindAttrName());
+  if (!attr)
+    return module.emitOpError()
+           << "expects '" << cir::CIRDialect::getOffloadKindAttrName()
+           << "' offload kind attribute";
+  if (attr.getValue() != expected)
+    return module.emitOpError()
+           << "expects '" << cir::CIRDialect::getOffloadKindAttrName()
+           << "' value '" << cir::stringifyOffloadKind(expected) << "'";
+  return success();
+}
+
+// A module marked with `cir.offload.container` holds the host module followed
+// by one or more device modules, each tagged with `cir.offload.kind`. Keeping
+// the host module first gives later offload passes a simple convention for
+// finding the host side while iterating the remaining device modules.
+static LogicalResult verifyOffloadContainer(mlir::Operation *op) {
+  auto container = mlir::dyn_cast<mlir::ModuleOp>(op);
+  if (!container)
+    return op->emitError() << "expects '"
+                           << cir::CIRDialect::getOffloadContainerAttrName()
+                           << "' attribute to be attached to '"
+                           << mlir::ModuleOp::getOperationName() << "'";
+
+  mlir::Block &body = *container.getBody();
+  if (body.empty())
+    return container.emitOpError()
+           << "expects host module as the first nested op";
+
+  auto host = mlir::dyn_cast<mlir::ModuleOp>(body.front());
+  if (!host)
+    return container.emitOpError()
+           << "expects host module as the first nested op";
+  if (failed(verifyOffloadKind(host, cir::OffloadKind::Host)))
+    return failure();
+
+  unsigned numDevices = 0;
+  auto it = body.begin();
+  for (++it; it != body.end(); ++it) {
+    auto module = mlir::dyn_cast<mlir::ModuleOp>(*it);
+    if (!module)
+      return container.emitOpError()
+             << "expects only nested builtin.module ops";
+    if (failed(verifyOffloadKind(module, cir::OffloadKind::Device)))
+      return failure();
+    ++numDevices;
+  }
+
+  if (numDevices == 0)
+    return container.emitOpError() << "expects at least one device module";
+  return success();
+}
+
+LogicalResult
+cir::CIRDialect::verifyOperationAttribute(mlir::Operation *op,
+                                          mlir::NamedAttribute attr) {
+  if (attr.getName() == getOffloadContainerAttrName()) {
+    if (!mlir::isa<mlir::UnitAttr>(attr.getValue()))
+      return op->emitError() << "expects '" << getOffloadContainerAttrName()
+                             << "' to be a unit attribute";
+    return verifyOffloadContainer(op);
+  }
+
+  // The container verifier owns the structural contract between a container
+  // and the modules it holds. All this can add is that the kind attribute
+  // never lands on something that is not a module.
+  if (attr.getName() == getOffloadKindAttrName() &&
+      !mlir::isa<mlir::ModuleOp>(op))
+    return op->emitError() << "expects '" << getOffloadKindAttrName()
+                           << "' attribute to be attached to '"
+                           << mlir::ModuleOp::getOperationName() << "'";
+
+  return success();
+}
+
 
//===----------------------------------------------------------------------===//
 // Helpers
 
//===----------------------------------------------------------------------===//
@@ -2375,66 +2481,6 @@ LogicalResult cir::VTTAddrPointOp::verify() {
   return success();
 }
 
-//===----------------------------------------------------------------------===//
-// OffloadContainerOp
-//===----------------------------------------------------------------------===//
-
-mlir::ModuleOp cir::OffloadContainerOp::getHostModule() {
-  return mlir::cast<mlir::ModuleOp>(getBody().front().front());
-}
-
-llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
-cir::OffloadContainerOp::getDeviceModules() {
-  mlir::Block &body = getBody().front();
-  auto begin = body.op_begin<mlir::ModuleOp>();
-  auto end = body.op_end<mlir::ModuleOp>();
-  if (begin != end)
-    ++begin;
-  return {begin, end};
-}
-
-static LogicalResult checkOffloadKind(mlir::ModuleOp module,
-                                      cir::OffloadKind expected) {
-  auto attr = module->getAttrOfType<cir::OffloadKindAttr>(
-      cir::CIRDialect::getOffloadKindAttrName());
-  if (!attr)
-    return module.emitOpError()
-           << "expects '" << cir::CIRDialect::getOffloadKindAttrName()
-           << "' offload kind attribute";
-  if (attr.getValue() != expected)
-    return module.emitOpError()
-           << "expects '" << cir::CIRDialect::getOffloadKindAttrName()
-           << "' value '" << cir::stringifyOffloadKind(expected) << "'";
-  return success();
-}
-
-LogicalResult cir::OffloadContainerOp::verify() {
-  mlir::Block &body = getBody().front();
-  if (body.empty())
-    return emitOpError() << "expects host module as the first nested op";
-
-  auto host = mlir::dyn_cast<mlir::ModuleOp>(body.front());
-  if (!host)
-    return emitOpError() << "expects host module as the first nested op";
-  if (failed(checkOffloadKind(host, cir::OffloadKind::Host)))
-    return failure();
-
-  unsigned numDevices = 0;
-  auto it = body.begin();
-  for (++it; it != body.end(); ++it) {
-    auto module = mlir::dyn_cast<mlir::ModuleOp>(*it);
-    if (!module)
-      return emitOpError() << "expects only nested builtin.module ops";
-    if (failed(checkOffloadKind(module, cir::OffloadKind::Device)))
-      return failure();
-    ++numDevices;
-  }
-
-  if (numDevices == 0)
-    return emitOpError() << "expects at least one device module";
-  return success();
-}
-
 
//===----------------------------------------------------------------------===//
 // FuncOp
 
//===----------------------------------------------------------------------===//
diff --git a/clang/test/CIR/IR/invalid-offload-container.cir 
b/clang/test/CIR/IR/invalid-offload-container.cir
index 219442f39674a..92d4213765489 100644
--- a/clang/test/CIR/IR/invalid-offload-container.cir
+++ b/clang/test/CIR/IR/invalid-offload-container.cir
@@ -1,54 +1,80 @@
 // RUN: cir-opt %s -verify-diagnostics -split-input-file
 
-module {
-  cir.offload.container {
-    builtin.module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} { // expected-error {{expects 'cir.offload.kind' 
value 'host'}}
-    }
+module attributes {cir.offload.container} {
+  module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} { 
// expected-error {{expects 'cir.offload.kind' value 'host'}}
   }
 }
 
 // -----
 
-module {
-  cir.offload.container {
-    builtin.module @host_0 attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
-    }
-    builtin.module @host_1 attributes {cir.offload.kind = 
#cir.offload_kind<host>} { // expected-error {{expects 'cir.offload.kind' value 
'device'}}
-    }
+module attributes {cir.offload.container} {
+  module @host_0 attributes {cir.offload.kind = #cir.offload_kind<host>} {
+  }
+  module @host_1 attributes {cir.offload.kind = #cir.offload_kind<host>} { // 
expected-error {{expects 'cir.offload.kind' value 'device'}}
   }
 }
 
 // -----
 
-module {
-  cir.offload.container {
-    builtin.module @host { // expected-error {{expects 'cir.offload.kind' 
offload kind attribute}}
-    }
+module attributes {cir.offload.container} {
+  module @host { // expected-error {{expects 'cir.offload.kind' offload kind 
attribute}}
+  }
+}
+
+// -----
+
+// expected-error@+1 {{expects only nested builtin.module ops}}
+module attributes {cir.offload.container} {
+  module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+  }
+  cir.func @f() {
+    cir.return
+  }
+}
+
+// -----
+
+// expected-error@+1 {{expects at least one device module}}
+module attributes {cir.offload.container} {
+  module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
   }
 }
 
 // -----
 
 module {
-  cir.offload.container { // expected-error {{expects only nested 
builtin.module ops}}
-    builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
-    }
-    cir.const #cir.int<0> : !cir.int<s, 32>
+  // The op below keeps the CIR dialect loaded, which is what makes the
+  // dialect attribute verifier run on the empty container module.
+  cir.func @f() {
+    cir.return
+  }
+  // expected-error@+1 {{expects host module as the first nested op}}
+  module @container attributes {cir.offload.container} {
   }
 }
 
 // -----
 
 module {
-  cir.offload.container { // expected-error {{expects at least one device 
module}}
-    builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
-    }
+  cir.func @f() {
+    cir.return {cir.offload.container} // expected-error {{expects 
'cir.offload.container' attribute to be attached to 'builtin.module'}}
   }
 }
 
 // -----
 
 module {
-  cir.offload.container { // expected-error {{expects host module as the first 
nested op}}
+  cir.func @f() {
+    cir.return {cir.offload.kind = #cir.offload_kind<host>} // expected-error 
{{expects 'cir.offload.kind' attribute to be attached to 'builtin.module'}}
+  }
+}
+
+// -----
+
+// expected-error@+1 {{expects 'cir.offload.container' to be a unit attribute}}
+module attributes {cir.offload.container = 0 : i32} {
+  module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+  }
+  module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
   }
 }
diff --git a/clang/test/CIR/IR/offload-container.cir 
b/clang/test/CIR/IR/offload-container.cir
index b7b53aa973ce5..f1e00bc63fed7 100644
--- a/clang/test/CIR/IR/offload-container.cir
+++ b/clang/test/CIR/IR/offload-container.cir
@@ -1,32 +1,28 @@
 // RUN: cir-opt %s -split-input-file --verify-roundtrip | FileCheck %s
 
-module {
-  cir.offload.container {
-    builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
-    }
-    builtin.module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
-    }
+module attributes {cir.offload.container} {
+  module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+  }
+  module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
   }
 }
 
-// CHECK: cir.offload.container {
-// CHECK:   builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
-// CHECK:   builtin.module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
+// CHECK: module attributes {cir.offload.container} {
+// CHECK:   module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
+// CHECK:   module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
 
 // -----
 
-module {
-  cir.offload.container {
-    builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
-    }
-    builtin.module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
-    }
-    builtin.module @device_1 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
-    }
+module attributes {cir.offload.container} {
+  module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {
+  }
+  module @device_0 attributes {cir.offload.kind = #cir.offload_kind<device>} {
+  }
+  module @device_1 attributes {cir.offload.kind = #cir.offload_kind<device>} {
   }
 }
 
-// CHECK: cir.offload.container {
-// CHECK:   builtin.module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
-// CHECK:   builtin.module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
-// CHECK:   builtin.module @device_1 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
+// CHECK: module attributes {cir.offload.container} {
+// CHECK:   module @host attributes {cir.offload.kind = 
#cir.offload_kind<host>} {
+// CHECK:   module @device_0 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {
+// CHECK:   module @device_1 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {

>From 1b10091e99962aca0245127138d9133e48382a17 Mon Sep 17 00:00:00 2001
From: David Rivera <[email protected]>
Date: Wed, 2 Sep 2026 01:32:56 -0400
Subject: [PATCH 4/5] address various coments

---
 .../include/clang/CIR/Dialect/IR/CIRAttrs.td  | 32 +++++++++--------
 .../include/clang/CIR/Dialect/IR/CIRDialect.h | 13 -------
 clang/lib/CIR/Dialect/IR/CIRDialect.cpp       | 35 +++----------------
 3 files changed, 23 insertions(+), 57 deletions(-)

diff --git a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td 
b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
index 691b0c311389d..cadb8d0fa1f84 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
@@ -129,28 +129,32 @@ def CIR_OffloadKind : CIR_I32EnumAttr<"OffloadKind", 
"offload kind", [
 def CIR_OffloadKindAttr : CIR_EnumAttr<CIR_OffloadKind, "offload_kind"> {
   let summary = "Offload kind of a module in an offload container";
   let description = [{
-    Marks a module as either the host module or one of the device modules of
-    an offload container, keeping a host CIR module and its associated device
-    CIR modules in one IR unit while offload merge/split pipeline passes need
-    visibility into both sides.
-
-    A module carrying the `cir.offload.container` unit attribute is such a
-    container. Its body holds only nested modules: the first one is the host
-    module and must carry `cir.offload.kind = #cir.offload_kind<host>`, and
-    all remaining ones are device modules and must carry
-    `cir.offload.kind = #cir.offload_kind<device>`. There must be at least one
-    device module. Keeping the host module first gives later passes a simple
-    convention for finding the host side while iterating the remaining device
-    modules.
+    Marks a module nested inside an offload container - identified by the
+    `cir.offload.container` unit attribute - as either the host module 
(carrying
+    `cir.offload.kind = #cir.offload_kind<host>`) or one of the device modules
+    (carrying `cir.offload.kind = #cir.offload_kind<device>`) of the offload
+    container. This creates a tiered module layout, where a singular offload
+    container contains a single host module and one or more device modules,
+    granting the offload merge/split pipeline passes the cross-boundary view.
 
     Example:
     ```mlir
     module attributes {cir.offload.container} {
       module @host attributes {cir.offload.kind = #cir.offload_kind<host>} {}
-      module @device attributes {cir.offload.kind = #cir.offload_kind<device>} 
{}
+      module @device1 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {}
+      module @device2 attributes {cir.offload.kind = 
#cir.offload_kind<device>} {}
     }
     ```
 
+    Note: In order to simplify offload passes, the offload container module
+    requires that the first module inside it is the host module and the rest
+    of the modules are device modules.
+
+    The nested modules are plain `builtin.module` ops rather than a dedicated
+    container op, so that the existing module infrastructure applies to them
+    unchanged; the layout above is enforced by the CIR dialect attribute
+    verifier, following the `gpu.container_module` precedent.
+
     The attribute names `cir.offload.container` and `cir.offload.kind` are
     defined by the `getOffloadContainerAttrName` and `getOffloadKindAttrName`
     methods in the CIRDialect class.
diff --git a/clang/include/clang/CIR/Dialect/IR/CIRDialect.h 
b/clang/include/clang/CIR/Dialect/IR/CIRDialect.h
index c61bd414036cb..2f1ef5b6cb9e0 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRDialect.h
+++ b/clang/include/clang/CIR/Dialect/IR/CIRDialect.h
@@ -88,19 +88,6 @@ RecordLayoutAttr getRecordLayout(mlir::ModuleOp mod, 
mlir::StringAttr name);
 /// Same lookup as getRecordLayout, but returns a null attribute instead of
 /// asserting when the record has no layout entry.
 RecordLayoutAttr tryGetRecordLayout(mlir::ModuleOp mod, mlir::StringAttr name);
-RecordLayoutAttr getRecordLayout(mlir::ModuleOp module, mlir::StringAttr name);
-
-/// Returns whether the module is an offload container, i.e. whether it carries
-/// the cir.offload.container unit attribute.  See CIR_OffloadKindAttr for the
-/// structure such a module is required to have.
-bool isOffloadContainer(mlir::ModuleOp module);
-
-/// Returns the host module of an offload container.
-mlir::ModuleOp getOffloadHostModule(mlir::ModuleOp container);
-
-/// Returns the device modules of an offload container, in container order.
-llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
-getOffloadDeviceModules(mlir::ModuleOp container);
 } // namespace cir
 
 // TableGen'erated files for MLIR dialects require that a macro be defined when
diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp 
b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index 45e3f011e6269..16ecedd10690b 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -111,30 +111,6 @@ Operation 
*cir::CIRDialect::materializeConstant(mlir::OpBuilder &builder,
                                  mlir::cast<mlir::TypedAttr>(value));
 }
 
-//===----------------------------------------------------------------------===//
-// Offload container helpers
-//===----------------------------------------------------------------------===//
-
-bool cir::isOffloadContainer(mlir::ModuleOp module) {
-  return module->hasAttr(cir::CIRDialect::getOffloadContainerAttrName());
-}
-
-mlir::ModuleOp cir::getOffloadHostModule(mlir::ModuleOp container) {
-  assert(isOffloadContainer(container) && "expected an offload container");
-  return mlir::cast<mlir::ModuleOp>(container.getBody()->front());
-}
-
-llvm::iterator_range<mlir::Block::op_iterator<mlir::ModuleOp>>
-cir::getOffloadDeviceModules(mlir::ModuleOp container) {
-  assert(isOffloadContainer(container) && "expected an offload container");
-  mlir::Block &body = *container.getBody();
-  auto begin = body.op_begin<mlir::ModuleOp>();
-  auto end = body.op_end<mlir::ModuleOp>();
-  if (begin != end)
-    ++begin;
-  return {begin, end};
-}
-
 
//===----------------------------------------------------------------------===//
 // Dialect attribute verification
 
//===----------------------------------------------------------------------===//
@@ -178,20 +154,19 @@ static LogicalResult 
verifyOffloadContainer(mlir::Operation *op) {
   if (failed(verifyOffloadKind(host, cir::OffloadKind::Host)))
     return failure();
 
-  unsigned numDevices = 0;
   auto it = body.begin();
-  for (++it; it != body.end(); ++it) {
+  ++it;
+  if (it == body.end())
+    return container.emitOpError() << "expects at least one device module";
+
+  for (; it != body.end(); ++it) {
     auto module = mlir::dyn_cast<mlir::ModuleOp>(*it);
     if (!module)
       return container.emitOpError()
              << "expects only nested builtin.module ops";
     if (failed(verifyOffloadKind(module, cir::OffloadKind::Device)))
       return failure();
-    ++numDevices;
   }
-
-  if (numDevices == 0)
-    return container.emitOpError() << "expects at least one device module";
   return success();
 }
 

>From e2cf561b191db9e4fead2cff042dfeaecbd0dc5d Mon Sep 17 00:00:00 2001
From: David Rivera <[email protected]>
Date: Mon, 7 Sep 2026 16:55:27 -0400
Subject: [PATCH 5/5] [CIR] Migrate CIR_OffloadKind off the legacy EnumAttrInfo
 hierarchy

CIR_I32EnumAttr and its genSpecializedAttr flag were removed by the
upstream enum migration (#220889); CIR_OffloadKind now derives from
CIR_I32Enum like the other CIR enums.
---
 clang/include/clang/CIR/Dialect/IR/CIRAttrs.td | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td 
b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
index cadb8d0fa1f84..ac6f2718d2fcd 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIRAttrs.td
@@ -119,12 +119,10 @@ def CIR_SourceLanguageAttr : 
CIR_EnumAttr<CIR_SourceLanguage, "lang"> {
 // OffloadKindAttr
 
//===----------------------------------------------------------------------===//
 
-def CIR_OffloadKind : CIR_I32EnumAttr<"OffloadKind", "offload kind", [
+def CIR_OffloadKind : CIR_I32Enum<"OffloadKind", "offload kind", [
   I32EnumAttrCase<"Host", 0, "host">,
   I32EnumAttrCase<"Device", 1, "device">
-]> {
-  let genSpecializedAttr = 0;
-}
+]>;
 
 def CIR_OffloadKindAttr : CIR_EnumAttr<CIR_OffloadKind, "offload_kind"> {
   let summary = "Offload kind of a module in an offload container";

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

Reply via email to